File Coverage

blib/lib/Test/Glade.pm
Criterion Covered Total %
statement 29 32 90.6
branch 3 4 75.0
condition 2 3 66.6
subroutine 9 10 90.0
pod n/a
total 43 49 87.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Test::Glade - a simple way to test Gtk2::GladeXML-based apps
4              
5             =head1 SYNOPSIS
6              
7             use Test::Glade tests => 2;
8              
9             my $glade_xml = 'interface.glade';
10             has_widget( $glade_xml, {
11             name => 'main_window',
12             type => 'GtkWindow',
13             properties => {
14             title => 'Test Application',
15             type => 'GTK_WINDOW_TOPLEVEL',
16             resizable => 1,
17             },
18             } );
19              
20             has_widget( $glade_xml, {
21             type => 'GtkButton',
22             properties => {label => 'Press me!'},
23             signals => {clicked => 'button_pressed_handler'},
24             } );
25              
26             =head1 DESCRIPTION
27              
28             GUIs are notoriously difficult to test. Historically this was well deserved
29             as the available perl GUI toolkits did not encourage separation of the view
30             and controller layers. The introduction of the Glade GUI designer and
31             Gtk2::GladeXML changed that by segregating user interface and logical
32             components (into GladeXML and Perl files respectively).
33              
34             Users who avoid creating GUI elements from within their application logic can
35             now test each layer separately with appropriate tools. The Perl logic can be
36             verified with standard unit tests and this module provides a way to inspect
37             and verify the GladeXML UI specification. You can confirm that a given widget
38             exists, that it has the correct label and other attributes, that it will be
39             correctly placed in the interface and that it will respond to signals
40             as expected.
41              
42             =head1 TEST METHODS
43              
44             =over 4
45              
46             =item has_widget($glade_file, $widget_desc, $test_name)
47              
48             Search for a widget in a GladeXML file. $widget is a hash reference of
49             widget attributes. See L for more information.
50              
51             =back
52              
53             =head1 OO METHODS
54              
55             If you have large GladeXML files, or want to perform many tests on each one,
56             it might be faster to use the object oriented interface. Files are only parsed
57             once, instead of once for each test.
58              
59             =over 4
60              
61             =item Test::Glade->new(file => $gladexml_file)
62              
63             Create a new Test::Glade object, passing in an optional GladeXML file.
64              
65             =item $test->load($gladexml_file)
66              
67             Load in a new GladeXML file.
68              
69             =item $test->widgets
70              
71             Return a list of all widgets in the file. See L for more
72             information.
73              
74             =item $test->find_widget($widget_desc)
75              
76             Find and return widget. Takes $widget_desc in the same format as has_widget().
77              
78             =back
79              
80             =head1 WIDGET DESCRIPTION
81              
82             =over 4
83              
84             =item name, type
85              
86             Scalars
87              
88             =item properties
89              
90             A hashref containing other widget properties, name => value
91              
92             =item signals
93              
94             A hashref of registered signal handlers, signal name => handler
95              
96             =item packing
97              
98             A hashref of packing attributes, name => value
99              
100             =item children
101              
102             A listref of child widgets
103              
104             =back
105              
106             =head1 WIDGET METHODS
107              
108             =over 4
109              
110             =item name, type, properties, children, signals, packing
111              
112             See the widget description section for return values.
113              
114             =back
115              
116             =head1 AUTHORS
117              
118             Nate Mueller
119              
120             =cut
121              
122             package Test::Glade::Obj;
123              
124 2     2   74458 use strict;
  2         7  
  2         85  
125 2     2   11 use warnings;
  2         3  
  2         178  
126 2     2   3482 use Data::Dumper;
  2         53523  
  2         896  
127              
128             sub new {
129 1     1   19 my ($class, %args) = @_;
130              
131 1         6 my $self = bless {%args}, $class;
132 1 50       18 $self->init if $self->can('init');
133 1         4 return $self;
134             }
135            
136             our $AUTOLOAD;
137             sub AUTOLOAD {
138 3     3   4627 my ($self) = @_;
139 3         24 my ($method) = $AUTOLOAD =~ /([^:]+)$/;
140 3 100 66     167 if ($method =~ /^_/ or not exists $self->{$method}) {
141 1         9 my @caller = caller(0);
142 1         190 die "No such method: $AUTOLOAD at $caller[1] line $caller[2]\n"
143             } else {
144 1         8 return $self->{$method};
145             }
146             }
147              
148 0     0     sub DESTROY { }
149              
150             package Test::Glade;
151              
152             our $VERSION = 1;
153              
154 2     2   183 use strict;
  2         4  
  2         81  
155 2     2   10 use warnings;
  2         4  
  2         88  
156 2     2   12 use base qw(Test::Glade::Obj Exporter);
  2         3  
  2         1279  
157              
158 2     2   1049 use XML::Parser;
  0         0  
  0         0  
159             use Test::Builder;
160              
161             my $test = Test::Builder->new;
162             our @EXPORT = qw(has_widget);
163              
164             sub import {
165             my ($self, @plan) = @_;
166             my $pack = caller;
167              
168             $test->exported_to($pack);
169             $test->plan(@plan);
170              
171             $self->export_to_level(1, $self, @EXPORT);
172             }
173              
174             sub init {
175             my ($self) = @_;
176             $self->load($self->{file}) if $self->{file};
177             }
178              
179             sub load {
180             my ($self, $file) = @_;
181             $self->{file} = $file;
182             my $parser = XML::Parser->new(Handlers => {
183             Init => sub { $_[0]->{self} = $self },
184             Start => \&_parse_start,
185             End => \&_parse_end,
186             Char => \&_parse_char,
187             });
188             $parser->parsefile($self->file);
189             }
190              
191             sub widgets {
192             my ($self) = @_;
193             return values %{$self->{widgets}};
194             }
195              
196             sub find_widget {
197             my ($self, $args) = @_;
198              
199             foreach my $widget ($self->widgets) {
200             return $widget if match($widget, $args);
201             }
202             return undef;
203             }
204              
205             sub has_widget {
206             my ($file, $args, $name) = @_;
207             $name ||= "has $args->{name}" if $args->{name};
208              
209             my $t = Test::Glade->new(file => $file);
210             $test->ok($t->find_widget($args), $name);
211             }
212              
213             sub match {
214             my ($a, $b) = @_;
215              
216             if (ref $b eq 'ARRAY') {
217             return 0 unless ref $a eq 'ARRAY';
218             foreach my $element (@$b) {
219             return 0 unless grep { match($_, $element) } @$a;
220             }
221             } elsif (ref $b eq 'HASH') {
222             return 0 unless ref $a eq 'HASH' || ref $a eq 'Test::Glade::Obj';
223             foreach my $key (keys %$b) {
224             return 0 unless exists $a->{$key};
225             return 0 unless match($a->{$key}, $b->{$key});
226             }
227             } else {
228             return 0 unless $a eq $b;
229             }
230             return 1;
231             }
232            
233             sub _parse_start {
234             my ($expat, $tag, %args) = @_;
235             my $self = $expat->{self};
236              
237             if ($tag eq 'widget') {
238             $self->{widgets}{$args{id}} = Test::Glade::Obj->new(
239             type => $args{class},
240             name => $args{id},
241             properties => {},
242             children => [],
243             packing => {},
244             signals => {},
245             );
246             push @{$self->{_active_widgets}}, $args{id};
247             } elsif ($tag eq 'property') {
248             $self->{_active_property} = $args{name};
249             } elsif ($tag eq 'packing') {
250             $self->{_packing} = 1;
251             } elsif ($tag eq 'signal') {
252             $self->{widgets}{$self->{_active_widgets}[-1]}{signals}{$args{name}} =
253             $args{handler};
254             }
255             }
256              
257             sub _parse_end {
258             my ($expat, $tag) = @_;
259             my $self = $expat->{self};
260              
261             if ($tag eq 'property') {
262             delete $self->{_active_property};
263             } elsif ($tag eq 'child') {
264             my $widget = $self->{widgets}{$self->{_active_widgets}[-1]};
265             my $parent = $self->{widgets}{$self->{_active_widgets}[-2]};
266             push @{$parent->{children}}, $widget;
267             pop @{$self->{_active_widgets}};
268             } elsif ($tag eq 'packing') {
269             delete $self->{_packing};
270             }
271             }
272              
273             sub _parse_char {
274             my ($expat, $char) = @_;
275             my $self = $expat->{self};
276             return unless $char =~ /\S/;
277             return unless $self->{_active_property};
278             return unless $self->{_active_widgets}[-1];
279              
280             if ($char eq 'False') { $char = 0 }
281             elsif ($char eq 'True') { $char = 1 }
282              
283             $self->{widgets}
284             {$self->{_active_widgets}[-1]}
285             {$self->{_packing} ? 'packing' : 'properties'}
286             {$self->{_active_property}} = $char;
287             }
288              
289             1;