The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -T

use strict; use warnings;
no warnings qw 'utf8 parenthesis regexp once qw bareword syntax';

use lib 't';
use HTML::DOM;
use HTML::DOM::EventTarget;
use HTML::DOM::Event ':all';

my $doc = new HTML::DOM;
{
	package MyEventTarget;
	our @ISA = HTML::DOM::EventTarget::;

	sub event_parent {
		${+shift}
	}
}

my $child = bless \do{my $x}, MyEventTarget=>;
my $grandchild = bless \do{my $x = $child}, 'MyEventTarget';

# -------------------------#
use tests 10; # (add|remove)EventListener and get_event_listeners

{
	my $sub1 = sub{};
	my $sub2 = sub{};
	my $sub3 = sub{};
	my $sub4 = sub{};
	is_deeply[$child->get_event_listeners('click')],[],
		'get_event_listeners initially returns nothing';
	is_deeply[$child->addEventListener(click=>$sub1)],[], 
		'addEventListener returns nothing';
	$child->addEventListener(click=>$sub2);
	is_deeply[sort $child->get_event_listeners('click')],
	         [sort $sub1, $sub2], 'get_event_listeners after adding 2';
	$child->addEventListener(click=>$sub3, 1);
	$child->addEventListener(click=>$sub4, 1);
	is_deeply[sort $child->get_event_listeners('click', 1)],
	         [sort $sub3, $sub4],
		'get_event_listeners (for capture phase) after adding 2';
	is_deeply[$child->removeEventListener(click=>$sub1)],[],
		'removeEventListener does nothing';
	is_deeply[$child->get_event_listeners('click')],
	         [$sub2],
		'get_event_listeners after removing one';
	$child->removeEventListener(click=>$sub3, 1);
	is_deeply[$child->get_event_listeners('click', 1)],
	         [$sub4],
		'get_event_listeners for capture phase after removing one';
	$child->addEventListener(focus => $sub3);
	$child->addEventListener(focus => $sub4);
	$child->addEventListener(focus => $sub2, 1);
	is_deeply[[$child->get_event_listeners('click')],
	          [$child->get_event_listeners('click', 1)],
	          [sort($child->get_event_listeners('focus'))],
	          [$child->get_event_listeners('focus', 1),]],
	         [[$sub2],
	          [$sub4],
	          [sort $sub3, $sub4],
	          [$sub2]],
	         'different slots for different event types and phases';
	$child->onsubmit($sub1);
	is +()=$child->get_event_listeners('submit'), 1,
	 'get_event_listeners with attribute event handlers';
	is +()=$child->get_event_listeners('submit',1), 0,
	 'event handlers to not apply to the capture phase';
}

# Let's clean up after ourselves:
clear_event_listeners($child, 'click', 'focus', 'submit');

sub clear_event_listeners {
	my $target = shift;
	for my $type(@_) {
		$target->removeEventListener($type, $_)
			for $target->get_event_listeners($type);
		$target->removeEventListener($type, $_, 1)
			for $target->get_event_listeners($type, 1);
	}
}


# -------------------------#
use tests 4; # event dispatch:
# First we'll make sure that the events are triggered in the right order,
# and for the right event type.

my $event = $doc->createEvent;
my $event2 = $doc->createEvent;
init $event type => click => cancellable => 1 => propagates_up => 1;
init $event2 type => focus => cancellable => 0 => propagates_up => 0;

our $e;

# some of these never get called--or shouldn't, if the module's work-
# ing correctly.
$child->addEventListener(click => sub { $e .= '-cclick1' });
$child->addEventListener(click => sub { $e .= '-cclick2' });
$child->addEventListener(click => sub { $e .= '-cclick1-capture' }, 1);
$child->addEventListener(click => sub { $e .= '-cclick2-capture' }, 1);
$grandchild->addEventListener(click => sub { $e .= '-gcclick1' });
$grandchild->addEventListener(click => sub { $e .= '-gcclick2' });
$grandchild->addEventListener(
	click => sub { $e .= '-gcclick1-capture' }, 1);
$grandchild->addEventListener(
	click => sub { $e .= '-gcclick2-capture' }, 1);
$child->addEventListener(focus => sub { $e .= '-cfocus1' });
$child->addEventListener(focus => sub { $e .= '-cfocus2' });
$child->addEventListener(focus => sub { $e .= '-cfocus1-capture' }, 1);
$child->addEventListener(focus => sub { $e .= '-cfocus2-capture' }, 1);
$grandchild->addEventListener(focus => sub { $e .= '-gcfocus1' });
$grandchild->addEventListener(focus => sub { $e .= '-gcfocus2' });
$grandchild->addEventListener(
	focus => sub { $e .= '-gcfocus1-capture' }, 1);
$grandchild->addEventListener(
	focus => sub { $e .= '-gcfocus2-capture' }, 1);

$@ = 'drit'; # Make sure event dispatch leaves this alone.

$e = '';
ok $grandchild->dispatchEvent($event), 'dispatchEvent returns true';
like $e, qr/^-cclick(\d)-capture      # Each pair can be run in any order,
             -cclick(?!\1)\d-capture  # hence the (\d) and (?!\1)\d.
             -gcclick(\d)
             -gcclick(?!\2)\d
             -cclick(\d)
             -cclick(?!\3)\d
         \z/x, 'order of fizzy event dispatch';

$e = '';
$grandchild->dispatchEvent($event2); # This event is not bubbly.
like $e, qr/^-cfocus(\d)-capture      # Each pair can be run in any order,
             -cfocus(?!\1)\d-capture  # hence the (\d) and (?!\1)\d.
             -gcfocus(\d)
             -gcfocus(?!\2)\d
          \z/x, 'order of flat event dispatch';

is $@, 'drit', 'event dispatch leaves $@ alone'; # bug in 0.033 and earlier

clear_event_listeners($child, 'click', 'focus');
clear_event_listeners($grandchild, 'click', 'focus');


# -------------------------#
use tests 1; # event dispatch:
# Now we need to see whether eventPhase is set correctly.

($event = $doc->createEvent)->initEvent(click => 1, 1);
$child->addEventListener(click => sub { $e .= $_[0]->eventPhase }, 1);
$child->addEventListener(click => sub { $e .= $_[0]->eventPhase }, 1);
$child->addEventListener(click => sub { $e .= $_[0]->eventPhase });
$child->addEventListener(click => sub { $e .= $_[0]->eventPhase });
$grandchild->addEventListener(click => sub { $e .= $_[0]->eventPhase });
$grandchild->addEventListener(click => sub { $e .= $_[0]->eventPhase });

$e = '';
$grandchild->dispatchEvent($event);
is $e, '112233', 'value of eventPhase during event dispatch';

clear_event_listeners($child, 'click');
clear_event_listeners($grandchild, 'click');


# -------------------------#
use tests 3; # event dispatch: stopPropagation

{
	# I put stopPropagation in both listeners for each phase, since
	# they could be called either order and I need to make sure that
	# the other handler at the same level is still called *after* the
	# first one has called stopPropagation.
	$child->addEventListener(click => my $capture1 = sub {
		$_[0]->stopPropagation;
		$e .= '-'
	}, 1);
	$child->addEventListener(click => my $capture2 = sub {
		$_[0]->stopPropagation;
		$e .= '-'
	}, 1);
	$grandchild->addEventListener(click => my $at_target1 = sub {
		$_[0]->stopPropagation; $e .= '='
	});
	$grandchild->addEventListener(click => my $at_target2 = sub {
		$_[0]->stopPropagation; $e .= '='
	});
	$child->addEventListener(click => my $fzz1 = sub {
		$_[0]->stopPropagation; $e .= '≡'
	});
	$child->addEventListener(click => my $fzz2 = sub {
		$_[0]->stopPropagation; $e .= '≡'
	});
	$doc->addEventListener(click => sub {
		$e = "You didn't expect this, did you?"
	});

	$e = '';
	($event = $doc->createEvent)->initEvent(click => 1, 1);
	$grandchild->dispatchEvent($event);
	is $e, '--', 'stopPropagation at capture phase';

	$child->removeEventListener(click => $_, 1)
		for $capture1, $capture2;

	$e = '';
	($event = $doc->createEvent)->initEvent(click => 1, 1);
	$grandchild->dispatchEvent($event);
	is $e, '==', 'stopPropagation at the target';

	$grandchild->removeEventListener(click => $_)
		for $at_target1, $at_target2;

	$e = '';
	($event = $doc->createEvent)->initEvent(click => 1, 1);
	$grandchild->dispatchEvent($event);
	is $e, '≡≡', 'stopPropagation at the bubbly phase';

}

clear_event_listeners($child, 'click');
clear_event_listeners($grandchild, 'click');
clear_event_listeners($doc, 'click');


# -------------------------#
use tests 10; # event dispatch:
#             qw/ target currentTarget preventDefault cancelable /
#    This section also makes sure that event types are indifferent to case.

$child->addEventListener(cLick => sub {
	is $_[0]->currentTarget, $child,
		'currentTarget at capture stage';
	is $_[0]->target, $grandchild,
		'"target" attr during capture phase';
}, 1);
$grandchild->addEventListener(clIck => sub {
	is $_[0]->currentTarget, $grandchild,
		'currentTarget at the target';
	is $_[0]->target, $grandchild,
		'"target" attr at the target';
});
$child->addEventListener(cliCk => sub {
	is scalar $_[0]->preventDefault, undef,
		'return val of preventDefault';
	is $_[0]->currentTarget, $child,
		'currentTarget while bubbles are being blown';
	is $_[0]->target, $grandchild,
		'"target" attr while froth is rising';
});

($event = $doc->createEvent)->initEvent(click => 1, 1);

ok! $grandchild->dispatchEvent($event),
	'preventDefault makes dispatchEvent return false';

clear_event_listeners($child, 'click');
clear_event_listeners($grandchild, 'click');

$grandchild->addEventListener(click => sub {
	$e = 'did it'; # make sure this handler is actually called
	$_[0]->preventDefault
});
($event = $doc->createEvent)->initEvent(click => 1, 0);
ok $grandchild->dispatchEvent($event),
	'preventDefault has no effect on uncancelable actions';
is $e, 'did it', 'And, yes, preventDefault *was* actually called.';

# -------------------------#
use tests 7; # event dispatch: event handlers
#  (accessors for event handlers are tested specifically further down)

clear_event_listeners($grandchild, 'click');
{
 ($event = $doc->createEvent)->initEvent('click',1,1);
 $grandchild->onclick(sub { 0 });
 ok !$grandchild->dispatchEvent($event),
  'defined false retval from attr event handler calls preventDefault';
 ($event = $doc->createEvent)->initEvent('click',1,1);
 $grandchild->onclick(sub{});
 ok $grandchild->dispatchEvent($event),
  'undef retval from attr event handler does not call preventDefault';
 ($event = $doc->createEvent)->initEvent('mouseover',1,1);
 $grandchild->onmouseover(sub{});
 ok $grandchild->dispatchEvent($event),
  'undef retval from onmouseover handler does not call preventDefault';
 ($event = $doc->createEvent)->initEvent('mouseover',1,1);
 $grandchild->onmouseover(sub{1});
 ok !$grandchild->dispatchEvent($event),
  'true retval from onmouseover handler calls preventDefault';
 my @scratch;
 *Function::call_with = sub { @scratch = @_ };
 $grandchild->onclick(bless[], 'Function');
 $grandchild->trigger_event('click');
 is $scratch[1], $grandchild, 'target is passed to call_with';
 isa_ok $scratch[2], 'HTML::DOM::Event', 'second arg to call_with';
 
 { package DelegatingEventTarget;
   our @ISA = HTML::DOM::EventTarget::;
   sub get_event_listeners { ${+shift}->get_event_listeners(@_) }
   sub event_handler       { ${+shift}->event_handler(@_) }
   sub addEventListener    { ${+shift}->addEventListener(@_) }
   sub removeEventListener { ${+shift}->removeEventListener(@_) }
 }
 my $delegate = bless \do{my $x = $grandchild}, DelegatingEventTarget::;
 $delegate->trigger_event('click');
 ($event = $doc->createEvent)->init(type => 'click');
 is $scratch[1], $delegate,
  'event handler wrappers can be transferred to other objects';
}

# -------------------------#
use tests 6; # exceptions thrown by dispatchEvent

$event = $doc->createEvent;
eval {
	$child->dispatchEvent($event);
};
isa_ok $@, 'HTML::DOM::Exception',
'$@ (after dispatchEvent with an uninited event)';
cmp_ok $@, '==', HTML::DOM::Exception::UNSPECIFIED_EVENT_TYPE_ERR,
    'dispatchEvent with an uninited event throws the ' .
    'appropriate error';

$event->initEvent(undef, 1, 1);
eval {
	$child->dispatchEvent($event);
};
isa_ok $@, 'HTML::DOM::Exception',
'$@ (after dispatchEvent with no event type)';
cmp_ok $@, '==', HTML::DOM::Exception::UNSPECIFIED_EVENT_TYPE_ERR,
    'dispatchEvent with an no event type throws the ' .
    'appropriate error';

$event->initEvent('' => 1, 1);
eval {
	$child->dispatchEvent($event);
};
isa_ok $@, 'HTML::DOM::Exception',
'$@ (after dispatchEvent with "" for the event type)';
cmp_ok $@, '==', HTML::DOM::Exception::UNSPECIFIED_EVENT_TYPE_ERR,
    'dispatchEvent with "" for the event type throws the ' .
    'appropriate error';




# -------------------------#
use tests 5; # trigger_event

clear_event_listeners($grandchild, 'click');

$grandchild->addEventListener(clink => sub {
	$_[0]->preventDefault
});

my @def = (default => sub {
	$e = $_[0];
});

$e = '';
($event = $doc->createEvent)->initEvent(clink => 1, 1);
$grandchild->trigger_event($event, @def);
is $e, '', 'event objects passed to trigger_event can be stopped';

$grandchild->trigger_event('clink', @def);
is $e, '', 'event names passed to trigger_event can be stopped';

$e = '';
($event = $doc->createEvent)->initEvent(clink => 1, 0);
$grandchild->trigger_event($event, @def);
is $e, $event,
    'the default event was run when an obj was passed to trigger_event';

clear_event_listeners($grandchild, 'clink');
$e = '';
$grandchild->trigger_event('clink', @def);
is $e->type, 'clink',
	'$event->type when an event name is passed to trigger_event';
is $e->target, $grandchild,
	'$event->target when an event name is passed to trigger_event';

undef $e; # remove circularities


# -------------------------#
use tests 1; # error_handler
# This doesn’t test $target->ownerDocument->error_handler; event-basics.t
# takes care of that.

{
	no warnings 'once';
	my $e;
	local *MyEventTarget'error_handler = sub{ sub{ $e = $@ }};
	$grandchild->addEventListener(foo => sub { die "67\n" });
	$grandchild->trigger_event('foo');
	is $e, "67\n", 'error_handler gets called';
}


# -------------------------#
use tests 7;  # event_listeners_enabled
{
	no warnings qw 'redefine once';
	my $e;
	local *MyEventTarget'event_listeners_enabled = sub{ 1 };
	$grandchild->addEventListener(foo => sub { ++$e });
	$grandchild->trigger_event('foo');
	is $e, 1,
	  'event handlers run when event_listeners_enabled returns true';
	local *MyEventTarget'event_listeners_enabled = sub{ 0 };
	$grandchild->trigger_event('foo');
	is $e, 1,
	  'event handlers don\'t run if event_listeners_enabled is false';
	local *MyEventTarget'event_listeners_enabled = sub{ 1 };
	local *MyDoc'event_listeners_enabled = sub { 0 };
	local *MyEventTarget'ownerDocument = sub { bless[], 'MyDoc' };
	$grandchild->trigger_event('foo');
	is $e, 2, 'An event_listeners_enabled method on the event';
	local *MyEventTarget'event_listeners_enabled = sub{ 0 };
	local *MyDoc'event_listeners_enabled = sub { 1 };
	$grandchild->trigger_event('foo');
	is $e, 2, ' target prevents ownerDocument from being checked.';
	undef *MyEventTarget'event_listeners_enabled;
	$grandchild->trigger_event('foo');
	is $e, 3, 'fallback to event_listeners_enabled';
	local *MyDoc'event_listeners_enabled = sub { 0 };
	$grandchild->trigger_event('foo');
	is $e, 3, '  on the ownerDocument';

	$grandchild->trigger_event('foo', default => sub {
	  is $_[0]->target, $grandchild,
	    'the event’s target is set when event handlers are disabled'
	    # something I got wrong at first
	});
	
}


# -------------------------#
use tests 8;  # on* and attr_event_listener
{
 my $scratch;
 my $sub1 = sub { $scratch .= "one called "};
 my $sub2 = sub {  $scratch .= "two called " };
 is +()=$child->onbdext, 0, 'null retval from on*';
 is +()=$child->onbdext($sub1),0,'null retval from on* initial assignment';
 is $child->onbdext($sub2), $sub1, 'on* returns old value';
 is $child->onbdext, $sub2, 'on* with no args after assignment';
 $child->trigger_event('bdext');
 is $scratch, "two called ",
  'on* registers event listener and removes old one';
 is $child->attr_event_listener('bdext'), $sub2,
  'attr_event_listener returns the same thing as on*';
 is $child->attr_event_listener('bdext',$sub1),$sub2,
  'setting attr_event_listener returns old val';
 is $child->onbdext, $sub1, 'and the change applies to on*';
}

# -------------------------#
use tests 1;  # error messages for invalid methods
{             # (Testing this is necessary since we implement AUTOLOAD.)
 eval { 'dwext'->HTML::DOM::Node::dwed; };
 like $@,
  qr/^Can't locate object method "dwed" via package "HTML::DOM::Node"/;
}