From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#!/usr/bin/perl -T
use strict; use warnings; no warnings qw 'utf8 parenthesis'; use lib 't';
use lib 't';
my $doc = new HTML::DOM;
my $event = $doc->createEvent('MutationEvents');
# -------------------------#
use tests 3; # constants
cmp_ok MODIFICATION , '==', 1, 'MODIFICATION';
cmp_ok ADDITION , '==', 2, 'ADDITION';
cmp_ok REMOVAL , '==', 3, 'REMOVAL';
# -------------------------#
use tests 13; # initMutationEvent
is +()=$event->relatedNode, 0, 'relatedNode before init';
is $event->prevValue, undef, 'prevValue before init';
is $event->newValue, undef, 'newValue before init';
is $event->attrName, undef, 'attrName before init';
is $event->attrChange, undef, 'attrChange before init';
my $foo = bless[];
is_deeply [initMutationEvent $event
DOMSubtreeModified => 1, 1,$foo,5,3,4,2
], [],
'initMutationEvent returns nothing';
ok bubbles $event, 'event is bubbly after init*Event';
ok cancelable $event, 'event is cancelable after init*Event';
is $event->relatedNode, $foo, 'relatedNode type after init*Event';
is $event->prevValue, 5, 'prevValue after init*Event';
is $event->newValue, 3, 'newValue, after init*Event';
is $event->attrName, 4, 'attrName after init*Event';
is $event->attrChange, 2, 'attrChange after init*Event';
# -------------------------#
use tests 7; # init
init $event type =>DOMSubtreeModified=>propagates_up=> 1,
cancellable=> 1, rel_node => $foo,
prev_value => 5, new_value => 3,
attr_name => 4, attr_change_type => 2;
ok bubbles $event, 'event is bubbly after init';
ok cancelable $event, 'event is cancelable after init';
is $event->relatedNode, $foo, 'relatedNode type after init';
is $event->prevValue, 5, 'prevValue after init';
is $event->newValue, 3, 'newValue, after init';
is $event->attrName, 4, 'attrName after init';
is $event->attrChange, 2, 'attrChange after init';
# -------------------------#
use tests 14; # trigger_event’s defaults
{
my $elem = $doc->createElement('div');
my $output;
$elem->addEventListener($_ => sub {
my $event = shift;
isa_ok $event, 'HTML::DOM::Event::Mutation',
$event->type . " event object";
$output = join ',', map {
my $foo = $event->$_;
ref $foo || (defined $foo ? $foo : '_')
} qw/ bubbles cancelable type relatedNode prevValue
newValue attrName attrChange /;
})
for map "DOM$_", qw(SubtreeModified NodeInserted NodeRemoved
NodeRemovedFromDocument
NodeInsertedIntoDocument AttrModified
CharacterDataModified);
undef $output;
$elem->trigger_event('DOMSubtreeModified');
is $output,
"1,0,DOMSubtreeModified,_,_,_,_,_";
undef $output;
$elem->trigger_event('DOMNodeInserted');
is $output,
"1,0,DOMNodeInserted,_,_,_,_,_";
undef $output;
$elem->trigger_event('DOMNodeRemoved');
is $output,
"1,0,DOMNodeRemoved,_,_,_,_,_";
undef $output;
$elem->trigger_event('DOMNodeRemovedFromDocument');
is $output,
"0,0,DOMNodeRemovedFromDocument,_,_,_,_,_";
undef $output;
$elem->trigger_event('DOMNodeInsertedIntoDocument');
is $output,
"0,0,DOMNodeInsertedIntoDocument,_,_,_,_,_";
undef $output;
$elem->trigger_event('DOMAttrModified');
is $output,
"1,0,DOMAttrModified,_,_,_,_,_";
undef $output;
$elem->trigger_event('DOMCharacterDataModified');
is $output,
"1,0,DOMCharacterDataModified,_,_,_,_,_";
};
# -------------------------#
sub gimme_a_test_doc {
my $doc = new HTML::DOM;
$doc->write(
'<html id=h>' .
'<body id=b>' .
'<div id=d1><p id=p11>a<p id=p12>a<p id=p13>a</div>' .
'<div id=d2><p id=p21>a<p id=p22>a<p id=p23>a</div>' .
'<div id=d3><p id=p31>a<p id=p32>a<p id=p33>a</div>'
);
$doc->close;
my $scratch = [];
for my$id(qw( h b d1 d2 d3 p11 p12 p13 p21 p22 p23 p31 p32 p33 )) {
my $e = $doc->getElementById($id);
$e->addEventListener($_ => sub {
push @$scratch, "$id-".$_[0]->target->id."-"
.lc$_[0]->type
})
for qw(
domsubtreemodified
domnoderemovedfromdocument
domnodeinsertedintodocument
);
# We need the evals around the $_[0]->foo->id things below,
# because at one point I had events erroneously occurring,
# with relatedNode set to undef.
$e->addEventListener($_ => sub {
push @$scratch,"$id-" . $_[0]->target->id ."-"
. lc ($_[0]->type) . "-"
. (eval{$_[0]->relatedNode->id}||'')
.'-'.(eval{$_[0]->target->parent->id}||'')
}) for qw ,domnodeinserted domnoderemoved,;
$e->addEventListener(domattrmodified => sub {
no warnings 'uninitialized';
push @$scratch, "$id-".$_[0]->target->id
."-domattrmodified-".
(eval{
return join '',$_->name,$_->value
for $_[0]->relatedNode
}||'')."-".
$_[0]->target->hasAttribute($_[0]->attrName)."-".
join '-', map $_[0]->$_,
attrName=>attrChange=>prevValue=>newValue=>;
});
$e->addEventListener(domcharacterdatamodified => sub {
push @$scratch, "$id-" .
(eval{$_[0]->target->id} ||
$_[0]->target->nodeName).
"-domcharacterdatamodified-".
join '-', map $_[0]->$_,
=>prevValue=>newValue=>;
});
}
$doc, $scratch;
}
# The basic order we have to test for is as follows. This is what happens
# when a node is moved from one tree to another. All the node-moving
# events, except attrmodified, follow the same order, but some steps may
# sometimes be skipped.
# event: noderemoved (node itself)
# event: noderemovedfromdocument (node and children)
# actual removal
# insertion
# event: nodeinserted
# event: nodeinsertedintodocument
# event: subtreemodified (nearest common parent)
# The event handlers above push the following info on to @$scratch, joined
# with hypens:
# current target id, target id or nodeName, event name
# possibly followed by more info based on the type of event:
# nodeinserted/removed: related node id, parent id
# attrmodfied: related node name and value,
# whether the attr exists, attr name,
# change type, prev value, new value
# characterdatamodified: -prev value-new value
# -------------------------#
use tests 6; # $node->insertBefore
{
my($doc, $scratch) = gimme_a_test_doc;
$doc->getElementById('d3')->insertBefore(
map $doc->getElementById($_), 'p21', 'p33'
);
is_deeply $scratch, [
'p21-p21-domnoderemoved-d2-d2' ,
'd2-p21-domnoderemoved-d2-d2' ,
'b-p21-domnoderemoved-d2-d2' ,
'h-p21-domnoderemoved-d2-d2' ,
'p21-p21-domnoderemovedfromdocument' ,
'p21-p21-domnodeinserted-d3-d3' ,
'd3-p21-domnodeinserted-d3-d3' ,
'b-p21-domnodeinserted-d3-d3' ,
'h-p21-domnodeinserted-d3-d3' ,
'p21-p21-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "insertBefore transferring an item to it's uncle (same level)";
@$scratch = ();
$doc->getElementById('d2')->insertBefore(
map $doc->getElementById($_), 'd1', 'p22'
);
is_deeply $scratch, [
'd1-d1-domnoderemoved-b-b' ,
'b-d1-domnoderemoved-b-b' ,
'h-d1-domnoderemoved-b-b' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-d2-d2' ,
'd2-d1-domnodeinserted-d2-d2' ,
'b-d1-domnodeinserted-d2-d2' ,
'h-d1-domnodeinserted-d2-d2' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "insertBefore transferring an item to it's nephew (down)";
@$scratch = ();
$doc->body->insertBefore(
map $doc->getElementById($_), 'd1', 'd2'
);
is_deeply $scratch, [
'd1-d1-domnoderemoved-d2-d2' ,
'd2-d1-domnoderemoved-d2-d2' ,
'b-d1-domnoderemoved-d2-d2' ,
'h-d1-domnoderemoved-d2-d2' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "insertBefore transferring an item to it's grandparent (up)";
my $foo = $doc->body->removeChild($doc->getElementById('d3'));
@$scratch = ();
$foo->insertBefore($doc->getElementById('d1'));
is_deeply $scratch, [
'd1-d1-domnoderemoved-b-b' ,
'b-d1-domnoderemoved-b-b' ,
'h-d1-domnoderemoved-b-b' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-d3-d3' ,
'd3-d1-domnodeinserted-d3-d3' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
'd3-d3-domsubtreemodified',
], "insertBefore transferring to an unrelated tree";
@$scratch = ();
$doc->body->insertBefore(
$foo->lastChild, $doc->getElementById('d2')
);
is_deeply $scratch, [
'd1-d1-domnoderemoved-d3-d3' ,
'd3-d1-domnoderemoved-d3-d3' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'd3-d3-domsubtreemodified',
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "insertBefore transferring from an unrelated tree";
$foo = $doc->body->removeChild($doc->getElementById('d1'));
@$scratch = ();
$doc->body->insertBefore(
$foo
);
is_deeply $scratch, [
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "insertBefore just inserting";
}
# -------------------------#
use tests 6; # $node->replaceChild
{
my($doc, $scratch) = gimme_a_test_doc;
$doc->getElementById('d3')->replaceChild(
map $doc->getElementById($_), 'p21', 'p33'
);
is_deeply $scratch, [
'p33-p33-domnoderemoved-d3-d3' ,
'd3-p33-domnoderemoved-d3-d3' ,
'b-p33-domnoderemoved-d3-d3' ,
'h-p33-domnoderemoved-d3-d3' ,
'p33-p33-domnoderemovedfromdocument',
'p21-p21-domnoderemoved-d2-d2' ,
'd2-p21-domnoderemoved-d2-d2' ,
'b-p21-domnoderemoved-d2-d2' ,
'h-p21-domnoderemoved-d2-d2' ,
'p21-p21-domnoderemovedfromdocument' ,
'p21-p21-domnodeinserted-d3-d3' ,
'd3-p21-domnodeinserted-d3-d3' ,
'b-p21-domnodeinserted-d3-d3' ,
'h-p21-domnodeinserted-d3-d3' ,
'p21-p21-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "replaceChild transferring an item to it's uncle (same level)";
@$scratch = ();
$doc->getElementById('d2')->replaceChild(
map $doc->getElementById($_), 'd1', 'p22'
);
is_deeply $scratch, [
'p22-p22-domnoderemoved-d2-d2' ,
'd2-p22-domnoderemoved-d2-d2' ,
'b-p22-domnoderemoved-d2-d2' ,
'h-p22-domnoderemoved-d2-d2' ,
'p22-p22-domnoderemovedfromdocument',
'd1-d1-domnoderemoved-b-b' ,
'b-d1-domnoderemoved-b-b' ,
'h-d1-domnoderemoved-b-b' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-d2-d2' ,
'd2-d1-domnodeinserted-d2-d2' ,
'b-d1-domnodeinserted-d2-d2' ,
'h-d1-domnodeinserted-d2-d2' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "replaceChild transferring an item to it's nephew (down)";
@$scratch = ();
my $d2 = $doc->body->replaceChild(
map $doc->getElementById($_), 'd1', 'd2'
);
is_deeply $scratch, [
'd2-d2-domnoderemoved-b-b' ,
'b-d2-domnoderemoved-b-b' ,
'h-d2-domnoderemoved-b-b' ,
'd2-d2-domnoderemovedfromdocument' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'p23-p23-domnoderemovedfromdocument' ,
'd1-d1-domnoderemoved-d2-d2' ,
'd2-d1-domnoderemoved-d2-d2' ,
'b-d1-domnoderemoved-d2-d2' ,
'h-d1-domnoderemoved-d2-d2' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'd2-d2-domsubtreemodified',
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "replaceChild transferring an item to it's grandparent (up)";
my $foo = $doc->body->removeChild($doc->getElementById('d3'));
@$scratch = ();
$foo->replaceChild($doc->getElementById('d1'), $foo->firstChild);
is_deeply $scratch, [
'p31-p31-domnoderemoved-d3-d3' ,
'd3-p31-domnoderemoved-d3-d3' ,
'd1-d1-domnoderemoved-b-b' ,
'b-d1-domnoderemoved-b-b' ,
'h-d1-domnoderemoved-b-b' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-d3-d3' ,
'd3-d1-domnodeinserted-d3-d3' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
'd3-d3-domsubtreemodified',
], "replaceChild transferring to an unrelated tree";
$doc->body->appendChild($d2);
@$scratch = ();
$doc->body->replaceChild(
$foo->firstChild, $d2
);
is_deeply $scratch, [
'd2-d2-domnoderemoved-b-b' ,
'b-d2-domnoderemoved-b-b' ,
'h-d2-domnoderemoved-b-b' ,
'd2-d2-domnoderemovedfromdocument' ,
'p23-p23-domnoderemovedfromdocument' ,
'd1-d1-domnoderemoved-d3-d3' ,
'd3-d1-domnoderemoved-d3-d3' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'd3-d3-domsubtreemodified',
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "replaceChild transferring from an unrelated tree";
$foo = $doc->body->removeChild($doc->getElementById('d1'));
$doc->body->appendChild($d2);
@$scratch = ();
$doc->body->replaceChild(
$foo, $d2
);
is_deeply $scratch, [
'd2-d2-domnoderemoved-b-b' ,
'b-d2-domnoderemoved-b-b' ,
'h-d2-domnoderemoved-b-b' ,
'd2-d2-domnoderemovedfromdocument' ,
'p23-p23-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "replaceChild inserting an orphaned node";
}
# -------------------------#
use tests 2; # $node->removeChild
{
my($doc, $scratch) = gimme_a_test_doc;
my $d3 = $doc->body->removeChild(
$doc->getElementById('d3')
);
is_deeply $scratch, [
'd3-d3-domnoderemoved-b-b' ,
'b-d3-domnoderemoved-b-b' ,
'h-d3-domnoderemoved-b-b' ,
'd3-d3-domnoderemovedfromdocument' ,
'p31-p31-domnoderemovedfromdocument' ,
'p32-p32-domnoderemovedfromdocument' ,
'p33-p33-domnoderemovedfromdocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "removeChild removing items from the document";
$d3->appendChild($doc->getElementById('d1'))
;@$scratch = ();
$d3->removeChild($d3->lastChild);
is_deeply $scratch, [
'd1-d1-domnoderemoved-d3-d3' ,
'd3-d1-domnoderemoved-d3-d3' ,
'd3-d3-domsubtreemodified' ,
], "removeChild removing an item from a node outside the doc";
}
# -------------------------#
use tests 6; # $node->appendChild
{
my($doc, $scratch) = gimme_a_test_doc;
$doc->getElementById('d3')->appendChild(
$doc->getElementById('p21')
);
is_deeply $scratch, [
'p21-p21-domnoderemoved-d2-d2' ,
'd2-p21-domnoderemoved-d2-d2' ,
'b-p21-domnoderemoved-d2-d2' ,
'h-p21-domnoderemoved-d2-d2' ,
'p21-p21-domnoderemovedfromdocument' ,
'p21-p21-domnodeinserted-d3-d3' ,
'd3-p21-domnodeinserted-d3-d3' ,
'b-p21-domnodeinserted-d3-d3' ,
'h-p21-domnodeinserted-d3-d3' ,
'p21-p21-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "appendChild transferring an item to it's uncle (same level)";
@$scratch = ();
$doc->getElementById('d2')->appendChild(
$doc->getElementById('d1')
);
is_deeply $scratch, [
'd1-d1-domnoderemoved-b-b' ,
'b-d1-domnoderemoved-b-b' ,
'h-d1-domnoderemoved-b-b' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-d2-d2' ,
'd2-d1-domnodeinserted-d2-d2' ,
'b-d1-domnodeinserted-d2-d2' ,
'h-d1-domnodeinserted-d2-d2' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "appendChild transferring an item to it's nephew (down)";
@$scratch = ();
$doc->body->appendChild(
$doc->getElementById('d1')
);
is_deeply $scratch, [
'd1-d1-domnoderemoved-d2-d2' ,
'd2-d1-domnoderemoved-d2-d2' ,
'b-d1-domnoderemoved-d2-d2' ,
'h-d1-domnoderemoved-d2-d2' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "appendChild transferring an item to it's grandparent (up)";
my $foo = $doc->body->removeChild($doc->getElementById('d3'));
@$scratch = ();
$foo->appendChild($doc->getElementById('d1'));
is_deeply $scratch, [
'd1-d1-domnoderemoved-b-b' ,
'b-d1-domnoderemoved-b-b' ,
'h-d1-domnoderemoved-b-b' ,
'd1-d1-domnoderemovedfromdocument' ,
'p11-p11-domnoderemovedfromdocument' ,
'p12-p12-domnoderemovedfromdocument' ,
'p13-p13-domnoderemovedfromdocument' ,
'd1-d1-domnodeinserted-d3-d3' ,
'd3-d1-domnodeinserted-d3-d3' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
'd3-d3-domsubtreemodified',
], "appendChild transferring to an unrelated tree";
@$scratch = ();
$doc->body->appendChild(
$foo->lastChild
);
is_deeply $scratch, [
'd1-d1-domnoderemoved-d3-d3' ,
'd3-d1-domnoderemoved-d3-d3' ,
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'd3-d3-domsubtreemodified',
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "appendChild transferring from an unrelated tree";
$foo = $doc->body->removeChild($doc->getElementById('d1'));
@$scratch = ();
$doc->body->appendChild(
$foo
);
is_deeply $scratch, [
'd1-d1-domnodeinserted-b-b' ,
'b-d1-domnodeinserted-b-b' ,
'h-d1-domnodeinserted-b-b' ,
'd1-d1-domnodeinsertedintodocument' ,
'p11-p11-domnodeinsertedintodocument' ,
'p12-p12-domnodeinsertedintodocument' ,
'p13-p13-domnodeinsertedintodocument' ,
'b-b-domsubtreemodified' ,
'h-b-domsubtreemodified' ,
], "appendChild just inserting";
}
# -------------------------#
use tests 9; # Element attribute mutations
{
my($doc, $scratch) = gimme_a_test_doc;
my $div = $doc->getElementById('d1');
$div->setAttribute("foo","bar");
is_deeply $scratch, [
'd1-d1-domattrmodified-foobar-1-foo-2--bar' ,
'b-d1-domattrmodified-foobar-1-foo-2--bar' ,
'h-d1-domattrmodified-foobar-1-foo-2--bar' ,
], 'setAttribute when the attr doesn\'t exist';
@$scratch = ();
# The presence of event listeners causes attr nodes to be autovivi-
# fied by events, so we need to reset it in order to test the right
# code path.
$div->attr("foo","bar");
$div->setAttribute("foo","barr");
is_deeply $scratch, [
'd1-d1-domattrmodified-foobarr-1-foo-1-bar-barr' ,
'b-d1-domattrmodified-foobarr-1-foo-1-bar-barr' ,
'h-d1-domattrmodified-foobarr-1-foo-1-bar-barr' ,
], 'setAttribute when the attr exists';
@$scratch = ();
$div->attr("foo","barr");
$div->removeAttribute("foo");
is_deeply $scratch, [
'd1-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
'b-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
'h-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
], 'removeAttribute when the attr exists';
@$scratch = ();
$div->removeAttribute("foo");
is_deeply $scratch, [
], 'removeAttribute when the attr existeth not';
$div->attr("foo","barr");
$div->getAttributeNode('foo');
$div->removeAttribute("foo");
is_deeply $scratch, [
'd1-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
'b-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
'h-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
], 'removeAttribute when the attr exists & is an auto-vivved node';
@$scratch = ();
my $attr = $doc->createAttribute('foo');;
$attr->value('bar');
$div->setAttributeNode($attr);
is_deeply $scratch, [
'd1-d1-domattrmodified-foobar-1-foo-2-bar-bar' ,
'b-d1-domattrmodified-foobar-1-foo-2-bar-bar' ,
'h-d1-domattrmodified-foobar-1-foo-2-bar-bar' ,
], 'setAttributeNode when the attribute doesn\'t exist';
@$scratch = ();
$attr = $doc->createAttribute('foo');;
$attr->value('barr');
$div->setAttributeNode($attr);
is_deeply $scratch, [
'd1-d1-domattrmodified-foobar-1-foo-3-bar-bar' ,
'b-d1-domattrmodified-foobar-1-foo-3-bar-bar' ,
'h-d1-domattrmodified-foobar-1-foo-3-bar-bar' ,
'd1-d1-domattrmodified-foobarr-1-foo-2-barr-barr' ,
'b-d1-domattrmodified-foobarr-1-foo-2-barr-barr' ,
'h-d1-domattrmodified-foobarr-1-foo-2-barr-barr' ,
], 'setAttributeNode when the attribute exists';
@$scratch = ();
$div->removeAttributeNode($attr);
is_deeply $scratch, [
'd1-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
'b-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
'h-d1-domattrmodified-foobarr--foo-3-barr-barr' ,
], 'removeAttributeNode when the attr exists';
$div->setAttributeNode($attr);
@$scratch = ();
$attr->value("onettn");
is_deeply $scratch, [
'd1-d1-domattrmodified-fooonettn-1-foo-1-barr-onettn' ,
'b-d1-domattrmodified-fooonettn-1-foo-1-barr-onettn' ,
'h-d1-domattrmodified-fooonettn-1-foo-1-barr-onettn' ,
], 'modification of an attr node directly';
# ~~~ All the shorthand HTML properties.
}
# -------------------------#
use tests 8; # Character data mutations
{
my($doc, $scratch) = gimme_a_test_doc;
my $node = $doc->getElementById('p11')->firstChild;
$node->data("stuff");
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-a-stuff' ,
'd1-#text-domcharacterdatamodified-a-stuff' ,
'b-#text-domcharacterdatamodified-a-stuff' ,
'h-#text-domcharacterdatamodified-a-stuff' ,
], 'data';
@$scratch = ();
$node->appendData("ing");
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-stuff-stuffing' ,
'd1-#text-domcharacterdatamodified-stuff-stuffing' ,
'b-#text-domcharacterdatamodified-stuff-stuffing' ,
'h-#text-domcharacterdatamodified-stuff-stuffing' ,
], 'appendData';
@$scratch = ();
$node->insertData(5,"er");
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-stuffing-stuffering' ,
'd1-#text-domcharacterdatamodified-stuffing-stuffering' ,
'b-#text-domcharacterdatamodified-stuffing-stuffering' ,
'h-#text-domcharacterdatamodified-stuffing-stuffering' ,
], 'insertData';
@$scratch = ();
$node->insertData16(5,"er");
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-stuffering-stufferering' ,
'd1-#text-domcharacterdatamodified-stuffering-stufferering' ,
'b-#text-domcharacterdatamodified-stuffering-stufferering' ,
'h-#text-domcharacterdatamodified-stuffering-stufferering' ,
], 'insertData16';
@$scratch = ();
$node->deleteData(5,2);
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-stufferering-stuffering' ,
'd1-#text-domcharacterdatamodified-stufferering-stuffering' ,
'b-#text-domcharacterdatamodified-stufferering-stuffering' ,
'h-#text-domcharacterdatamodified-stufferering-stuffering' ,
], 'appendData';
@$scratch = ();
$node-> deleteData16(1,1);
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-stuffering-suffering' ,
'd1-#text-domcharacterdatamodified-stuffering-suffering' ,
'b-#text-domcharacterdatamodified-stuffering-suffering' ,
'h-#text-domcharacterdatamodified-stuffering-suffering' ,
], 'deleteData16';
@$scratch = ();
$node->replaceData(4,5,'olk');
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-suffering-suffolk' ,
'd1-#text-domcharacterdatamodified-suffering-suffolk' ,
'b-#text-domcharacterdatamodified-suffering-suffolk' ,
'h-#text-domcharacterdatamodified-suffering-suffolk' ,
], 'replaceData';
@$scratch = ();
$node->replaceData16(0,1,"S");
is_deeply $scratch, [
'p11-#text-domcharacterdatamodified-suffolk-Suffolk' ,
'd1-#text-domcharacterdatamodified-suffolk-Suffolk' ,
'b-#text-domcharacterdatamodified-suffolk-Suffolk' ,
'h-#text-domcharacterdatamodified-suffolk-Suffolk' ,
], 'replaceData16';
}
# -------------------------#
use tests 8; # Mutations affecting Attr nodes
{
my($doc, $scratch) = gimme_a_test_doc;
my $div = $doc->getElementById('d1');
$div->setAttribute("foo","barr"); # We want an attribute that
# already exists;
@$scratch = ();
my $attr = $div->getAttributeNode('foo');
$attr->addEventListener('DOMCharacterDataModified' => sub {
my $event = shift;
push @$scratch, join '-',
$event->currentTarget->name,
$event->target->nodeName,
lc $event->type,
});
$attr->addEventListener($_ => sub {
my $event = shift;
push @$scratch, join '-',
$event->currentTarget->name,
$event->target->nodeName,
lc $event->type,
}) for qw/ DOMNodeInserted DOMNodeInsertedIntoDocument
DOMNodeRemoved DOMNodeRemovedFromDocument
DOMSubtreeModified /;
$attr->value("barr");
is_deeply $scratch, [
], 'attr->value(...) the same as the existing value';
$attr->nodeValue("barr");
is_deeply $scratch, [
], 'attr->nodeValue(...) the same as the existing value';
$attr->value("barrel");
is_deeply $scratch, [
'foo-#text-domcharacterdatamodified' ,
'd1-d1-domattrmodified-foobarrel-1-foo-1-barr-barrel' ,
'b-d1-domattrmodified-foobarrel-1-foo-1-barr-barrel' ,
'h-d1-domattrmodified-foobarrel-1-foo-1-barr-barrel' ,
], 'attr->value(new_value)';
@$scratch = ();
$attr->nodeValue("d barrel");
is_deeply $scratch, [
'foo-#text-domcharacterdatamodified' ,
'd1-d1-domattrmodified-food barrel-1-foo-1-barrel-d barrel' ,
'b-d1-domattrmodified-food barrel-1-foo-1-barrel-d barrel' ,
'h-d1-domattrmodified-food barrel-1-foo-1-barrel-d barrel' ,
], 'attr->nodeValue(new)';
@$scratch = ();
$attr->firstChild->data("foo");
is_deeply $scratch, [
'foo-#text-domcharacterdatamodified' ,
'd1-d1-domattrmodified-foofoo-1-foo-1-d barrel-foo' ,
'b-d1-domattrmodified-foofoo-1-foo-1-d barrel-foo' ,
'h-d1-domattrmodified-foofoo-1-foo-1-d barrel-foo' ,
], 'direct modification of the text node';
@$scratch = ();
my $tn1 = $attr->firstChild,
my $tn2 = $doc->createTextNode('led');
for my $n($tn1, $tn2) {
$n->addEventListener($_=>sub{
push@$scratch,(lc shift->type)
}) for map "domnode${_}document",
'insertedinto', 'removedfrom'
}
$attr->replaceChild($tn2,$tn1);
is_deeply $scratch, [
'foo-#text-domnoderemoved' ,
'domnoderemovedfromdocument' ,
'foo-#text-domnodeinserted' ,
'domnodeinsertedintodocument' ,
'foo-foo-domsubtreemodified',
'd1-d1-domattrmodified-fooled-1-foo-1-foo-led' ,
'b-d1-domattrmodified-fooled-1-foo-1-foo-led' ,
'h-d1-domattrmodified-fooled-1-foo-1-foo-led' ,
], 'attr->replaceChild still attached to the doc';
$div->detach;
@$scratch = ();
$attr->replaceChild($tn1,$tn2);
is_deeply $scratch, [
'foo-#text-domnoderemoved' ,
'foo-#text-domnodeinserted' ,
'foo-foo-domsubtreemodified',
'd1-d1-domattrmodified-foofoo-1-foo-1-led-foo' ,
], 'attr->replaceChild with its elem detached';
$doc->body->push_content($div);
$div->removeAttribute('foo');
@$scratch = ();
$attr->replaceChild($tn2,$tn1);
is_deeply $scratch, [
'foo-#text-domnoderemoved' ,
'foo-#text-domnodeinserted' ,
'foo-foo-domsubtreemodified',
], 'attr->replaceChild, the attr itself detached';
# ~~~ need a test for moving a text node that is in use on to the
# attr
}
# -------------------------#
use tests 3; # Default event handlers triggered by mutation events
# (there’s special handling for this case [auto-vivacious
{ # events] in EventTarget.pm/trigger_event)
my $doc = new HTML::DOM; $doc->open;
my $e;
$doc->default_event_handler(sub { $e = shift });
$doc->body->setAttribute("foo","bar");
isa_ok $e, 'HTML::DOM::Event',
'event auto-vivved solely for deh’s sake';
is $e->target, $doc->body, 'target is set correctly';
is $e->type, 'DOMAttrModified', 'type is set correctly';
}
# -------------------------#
use tests 444; # MutationEvents caused by DOM Level 0 attribute attributes
{
my $doc = new HTML::DOM;
my %attrnames = # for where the method differs from the attrname
qw(
className class
httpEquiv http-equiv
acceptCharset accept-charset
defaultSelected selected
defaultValue value
htmlFor for
ch char
chOff charoff
defaultChecked checked
);
my %booleans = map +($_=>1), # boolean attributes
qw< disabled multiple defaultSelected readOnly isMap
defaultChecked compact noShade declare defer noWrap noResize >;
for(
[span => qw[ id title lang dir className ]],
[html => qw[ version ]],
[head => qw[ profile ]],
[link => qw[ charset href hreflang media rel rev target
type ]],
[meta => qw[ content name scheme httpEquiv ]],
[base => qw[ href target ]],
[isindex => qw[ prompt ]],
[style => qw[ media type ]],
[body => qw[ aLink background bgColor link text
vLink ]],
[form => qw[ name action enctype method target
acceptCharset]],
[select => qw[ name size tabIndex ]],
[optgroup => qw[ label ]],
[option => qw[ label ]],
[input => qw[ accept accessKey align alt maxLength name
size src tabIndex type useMap ]],
[textarea => qw[ accessKey cols name rows tabIndex ]],
[button => qw[ accessKey name tabIndex ]],
[label => qw[ accessKey htmlFor ]],
[legend => qw[ accessKey align ]],
[ul => qw[ type ]],
[ol => qw[ start type ]],
[li => qw[ type value ]],
[div => qw[ align ]],
[p => qw[ align ]],
[h1 => qw[ align ]],
[q => qw[ cite ]],
[pre => qw[ width ]],
[br => qw[ clear ]],
[basefont => qw[ color face size ]],
[font => qw[ color face size ]],
[hr => qw[ align size width ]],
[ins => qw[ cite dateTime ]],
[a => qw[ accessKey charset coords href hreflang
name rel rev shape tabIndex target
type ]],
[img => qw[ name align alt border height hspace isMap
longDesc src useMap vspace width ]],
[object => qw[ code align archive border codeBase
codeType data height hspace name standby
tabIndex type useMap vspace width ]],
[param => qw[ name type value valueType ]],
[applet => qw[ align alt archive code codeBase height
hspace name object vspace width ]],
[map => qw[ name ]],
[area => qw[ accessKey alt coords href shape tabIndex
target ]],
[script => qw[ event charset src type htmlFor ]],
[table => qw[ align bgColor border cellPadding
cellSpacing frame rules summary width ]],
[caption => qw[ align ]],
[col => qw[ align span vAlign width ch chOff ]],
[tbody => qw[ align vAlign ch chOff ]],
[tr => qw[ align bgColor vAlign ch chOff ]],
[td => qw[ abbr align axis bgColor colSpan headers
height rowSpan scope vAlign width
ch chOff ]],
[frameset => qw[ cols rows ]],
[frame => qw[ frameBorder longDesc marginHeight
marginWidth name scrolling src ]],
[iframe => qw[ align frameBorder height longDesc
marginHeight marginWidth name scrolling
src width ]],
[select => qw[ disabled multiple ]],
[optgroup => qw[ disabled ]],
[option => qw[ disabled defaultSelected ]],
[input => qw[ disabled readOnly defaultValue
defaultChecked]],
[textarea => qw[ disabled readOnly ]],
[button => qw[ disabled ]],
[ul => qw[ compact ]],
[ol => qw[ compact ]],
[dl => qw[ compact ]],
[dir => qw[ compact ]],
[menu => qw[ compact ]],
[hr => qw[ noShade ]],
[object => qw[ declare ]],
[script => qw[ defer ]],
[td => qw[ noWrap ]],
[frame => qw[ noResize ]],
) {
my $e = $doc->createElement(my $tag = shift @$_);
my @scratch;
# copied and pasted from gimme_a_test_doc (and tweaked):
$e->addEventListener(domattrmodified => sub {
no warnings 'uninitialized';
push @scratch,
(eval{
return join '',$_->name,$_->value
for $_[0]->relatedNode
}||'')."-".
$_[0]->target->hasAttribute($_[0]->attrName)."-".
join '-', map lc $_[0]->$_,
attrName=>attrChange=>prevValue=>newValue=>;
});
for my $attr_m( @$_ ) {
# attr_m == attribute method; _n == name
my $attr_n = $attrnames{$attr_m} || lc $attr_m;
my $is_bool = $booleans{$attr_m};
@scratch = ();
eval { $e->$attr_m("foo");}; $@ and (warn tag $e),die;
# Test right here --------
my $testval = $is_bool ? $attr_n : 'foo';
is_deeply \@scratch, [
"${attr_n}$testval-1-$attr_n-2--$testval" ,
], "$tag elem ->$attr_m(foo) creating the attr";
next if $is_bool; # The next test doesn’t apply to
@scratch = (); # boolean attrs.
$e->$attr_m("bar");
# A second test right here --
is_deeply \@scratch, [
"${attr_n}bar-1-$attr_n-1-foo-bar" ,
], "$tag elem ->$attr_m(bar) changing the attr";
}
}
}