Chandra

 view release on metacpan or  search on metacpan

t/52_contextmenu_edge.t  view on Meta::CPAN

#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;

use Chandra::ContextMenu;

# ---- Empty menu ----
{
    my $cm = Chandra::ContextMenu->new;
    my $js = $cm->js_code;
    like($js, qr/items=\[\]/, 'empty items in JS');
}

# ---- Deeply nested submenus ----
{
    my $deep = { label => 'L5', action => sub {} };
    for my $i (reverse 1..4) {
        $deep = { label => "L$i", submenu => [$deep] };
    }
    my $cm = Chandra::ContextMenu->new(items => [$deep]);
    $cm->attach('#x');
    my $js = $cm->js_code;
    like($js, qr/sub:/, 'nested submenus in JS');
    ok(length($js) > 100, 'JS code generated for deep nesting');
}

# ---- Very long label ----
{
    my $long = 'A' x 500;
    my $cm = Chandra::ContextMenu->new(
        items => [{ label => $long, action => sub {} }],
    );
    my $it = $cm->items->[0];
    is(length($it->{label}), 500, 'long label stored');
    $cm->attach('#x');
    my $js = $cm->js_code;
    like($js, qr/A{100}/, 'long label in JS');
}

# ---- Attach to nonexistent selector (no crash) ----
{
    my $cm = Chandra::ContextMenu->new;
    eval { $cm->attach('#does-not-exist') };
    ok(!$@, 'attach nonexistent selector no error');
    my @att = $cm->attachments;
    is(scalar @att, 1, 'selector stored even if nonexistent');
}

# ---- Duplicate labels ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [
            { label => 'Dup', action => sub { 'first' } },
            { label => 'Dup', action => sub { 'second' } },
        ],
    );
    is(scalar @{$cm->items}, 2, 'duplicate labels allowed');
    # set_item only modifies first match
    $cm->set_item('Dup', disabled => 1);
    ok($cm->items->[0]{disabled}, 'first dup modified');
    ok(!$cm->items->[1]{disabled}, 'second dup not modified');
}

# ---- Unicode labels ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [
            { label => "\x{2603} Snowman", action => sub {} },
            { label => "Caf\x{e9}", action => sub {} },
        ],
    );
    like($cm->items->[0]{label}, qr/Snowman/, 'unicode label stored');
    $cm->attach('#x');
    my $js = $cm->js_code;
    like($js, qr/Snowman/, 'unicode in JS output');
}

# ---- Many items ----
{
    my @items = map { { label => "Item $_", action => sub {} } } 1..100;
    my $cm = Chandra::ContextMenu->new(items => \@items);
    is(scalar @{$cm->items}, 100, '100 items stored');
    $cm->attach('#x');
    my $js = $cm->js_code;
    like($js, qr/Item 100/, 'all items in JS');
}

# ---- Remove nonexistent item (no crash) ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [{ label => 'A', action => sub {} }],
    );
    eval { $cm->remove_item('NOPE') };
    ok(!$@, 'remove nonexistent no crash');
    is(scalar @{$cm->items}, 1, 'items unchanged');
}

# ---- set_item nonexistent (no crash) ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [{ label => 'A', action => sub {} }],
    );
    eval { $cm->set_item('NOPE', disabled => 1) };
    ok(!$@, 'set_item nonexistent no crash');
}

# ---- Add then remove ----
{
    my $cm = Chandra::ContextMenu->new;
    $cm->add_item({ label => 'X', action => sub {} });
    $cm->add_item({ label => 'Y', action => sub {} });
    $cm->add_item({ label => 'Z', action => sub {} });
    is(scalar @{$cm->items}, 3, '3 items');

    $cm->remove_item('Y');
    is(scalar @{$cm->items}, 2, '2 items after remove middle');
    is($cm->items->[0]{label}, 'X', 'X remains');
    is($cm->items->[1]{label}, 'Z', 'Z remains');
}

# ---- Separator-only menu ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [
            { separator => 1 },
            { separator => 1 },
        ],
    );
    $cm->attach('#x');
    my $js = $cm->js_code;
    like($js, qr/sep:1/, 'separator-only menu JS ok');
}

# ---- Dispatch malformed JSON ----
{
    my $cm = Chandra::ContextMenu->new;
    eval { $cm->_dispatch('') };
    ok(!$@, 'empty string dispatch no crash');
    eval { $cm->_dispatch('{}') };
    ok(!$@, 'empty object dispatch no crash');
    eval { $cm->_dispatch('{"type":"unknown"}') };
    ok(!$@, 'unknown type dispatch no crash');
}

# ---- Dispatch action with nonexistent ID ----
{
    my $cm = Chandra::ContextMenu->new;
    eval { $cm->_dispatch('{"type":"action","id":9999}') };
    ok(!$@, 'nonexistent action ID no crash');
}

# ---- Multiple attach/detach cycles ----
{
    my $cm = Chandra::ContextMenu->new;
    for my $i (1..10) {
        $cm->attach("#sel-$i");
    }
    my @att = $cm->attachments;
    is(scalar @att, 10, '10 attachments');
    for my $i (1..5) {
        $cm->detach("#sel-$i");
    }
    @att = $cm->attachments;
    is(scalar @att, 5, '5 remaining');
}

# ---- Items without action (display-only) ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [
            { label => 'Info', disabled => 1 },
        ],
    );
    $cm->attach('#x');
    my $js = $cm->js_code;
    like($js, qr/dis:1/, 'disabled no-action item in JS');
}

# ---- Action error handling ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [
            { label => 'Boom', action => sub { die "boom" } },
        ],
    );
    $cm->attach('#x');
    my $id = $cm->items->[0]{_id};
    my $warned = '';
    local $SIG{__WARN__} = sub { $warned = $_[0] };
    eval { $cm->_dispatch('{"type":"action","id":' . $id . '}') };
    like($warned, qr/boom/, 'action error warned');
}

# ---- Global with dynamic callback ----
{
    my $cm = Chandra::ContextMenu->new;
    my $cb_called = 0;
    $cm->attach_global(sub { $cb_called++; return [{ label => 'Dyn' }] });
    # Can't fully test without app, just verify no crash
    ok(1, 'attach_global with dynamic callback ok');
    $cm->detach_global;
    ok(1, 'detach_global after dynamic ok');
}

# ---- js_code with all features ----
{
    my $cm = Chandra::ContextMenu->new(
        items => [
            { label => 'Edit', icon => "\x{270f}", shortcut => 'Ctrl+E',
              action => sub {} },
            { separator => 1 },
            { label => 'Toggle', checkable => 1, checked => 0,
              action => sub {} },
            { label => 'More', submenu => [
                { label => 'Sub1', action => sub {} },
            ]},
            { label => 'Disabled', disabled => 1, action => sub {} },
        ],
    );
    $cm->attach_global;
    my $js = $cm->js_code;
    like($js, qr/ico:'/, 'icon in JS');
    like($js, qr/sc:'Ctrl\+E'/, 'shortcut in JS');
    like($js, qr/chk:1/, 'checkable in JS');
    like($js, qr/sub:\[/, 'submenu in JS');
    like($js, qr/dis:1/, 'disabled in JS');
    like($js, qr/isGlobal=1/, 'global flag in JS');
}

done_testing;



( run in 1.337 second using v1.01-cache-2.11-cpan-39bf76dae61 )