The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!/usr/bin/perl -T
use strict; use warnings;
no warnings qw 'utf8 parenthesis regexp once qw bareword syntax';
use lib 't';
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';
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"/;
}