Bugzilla – Attachment 192076 Details for
Bug 41719
Add rendering test coverage for Labels and Patroncards
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41719: Add rendering test coverage for Labels and Patroncards
da65744.patch (text/plain), 21.39 KB, created by
Chris Nighswonger
on 2026-01-27 03:14:40 UTC
(
hide
)
Description:
Bug 41719: Add rendering test coverage for Labels and Patroncards
Filename:
MIME Type:
Creator:
Chris Nighswonger
Created:
2026-01-27 03:14:40 UTC
Size:
21.39 KB
patch
obsolete
>From da65744c6bf6264c1552b0bf38191534c5917244 Mon Sep 17 00:00:00 2001 >From: Chris Nighswonger <chris.nighswonger@veritassuperaitsolutions.com> >Date: Tue, 27 Jan 2026 00:34:19 +0000 >Subject: [PATCH] Bug 41719: Add rendering test coverage for Labels and > Patroncards > >Extends t/db_dependent/Labels/t_Label.t with subtests for: >- draw_label_text() structure validation >- draw_guide_box() PDF stream output >- All supported barcode types (CODE39, CODE39MOD, CODE39MOD10, > COOP2OF5, INDUSTRIAL2OF5, EAN13) >- create_label() printing type orchestration (BIB/BAR/BIBBAR/BARBIB) > >Creates t/db_dependent/Patroncards/t_Patroncard.t with subtests for: >- draw_guide_box(), draw_guide_grid(), draw_text() >- draw_barcode(), draw_image() >- End-to-end PDF output > >AI Assistance: Claude Opus 4.5 drafted test code and debugged a >PDF::Reuse issue (GitHub cnighswonger/PDF-Reuse#24, fixed in 0.43). >Human author directed the approach, rejected initial implementation >that merely exercised PDF::Reuse rather than Koha code, reviewed all >changes, and verified tests pass. > >Assisted-by: Claude Opus 4.5 (Anthropic) >--- > t/db_dependent/Labels/t_Label.t | 176 +++++++++- > t/db_dependent/Patroncards/t_Patroncard.t | 387 ++++++++++++++++++++++ > 2 files changed, 558 insertions(+), 5 deletions(-) > create mode 100644 t/db_dependent/Patroncards/t_Patroncard.t > >diff --git a/t/db_dependent/Labels/t_Label.t b/t/db_dependent/Labels/t_Label.t >index 9eb967a2bb..d0a13a649e 100755 >--- a/t/db_dependent/Labels/t_Label.t >+++ b/t/db_dependent/Labels/t_Label.t >@@ -2,7 +2,7 @@ > > # This file is part of Koha. > # >-# Copyright 2020 Koha Development team >+# Copyright 2020, 2026 Koha Development team > # Copyright (C) 2017 Mark Tompsett > # > # Koha is free software; you can redistribute it and/or modify it >@@ -21,25 +21,26 @@ > use Modern::Perl; > > use Test::NoWarnings; >-use Test::More tests => 7; >+use Test::More tests => 11; > use t::lib::TestBuilder; > use t::lib::Mocks; > > use MARC::Record; > use MARC::Field; > use Data::Dumper; >+use File::Temp qw( tempfile ); > > use C4::Items; > use C4::Biblio; > use C4::Labels::Layout; >+use C4::Creators::PDF; > > use Koha::Database; > > use_ok('C4::Labels::Label'); > >-my $database = Koha::Database->new(); >-my $schema = $database->schema(); >-$schema->storage->txn_begin(); >+my $schema = Koha::Database->new->schema; >+$schema->storage->txn_begin; > > my $batch_id; > my ( $llx, $lly ) = ( 0, 0 ); >@@ -152,6 +153,171 @@ is_deeply( > is( $barcode_width, '2.104', ); > is( $barcode_height, '0.01', ); > >+# ======================================== >+# Subtest: draw_label_text() deep assertions >+# ======================================== >+subtest 'draw_label_text() returns correct text structure' => sub { >+ plan tests => 8; >+ >+ my $label_bib = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ printing_type => 'BIB', >+ justify => 'L', >+ ); >+ my $text_result = $label_bib->create_label(); >+ >+ ok( defined $text_result, 'create_label(BIB) returns defined value' ); >+ is( ref $text_result, 'ARRAY', 'Return value is an arrayref' ); >+ >+SKIP: { >+ skip 'No label text returned', 6 unless ref $text_result eq 'ARRAY' && @$text_result; >+ >+ my $first_line = $text_result->[0]; >+ ok( exists $first_line->{'text_llx'}, 'First line has text_llx key' ); >+ ok( exists $first_line->{'text_lly'}, 'First line has text_lly key' ); >+ ok( exists $first_line->{'font'}, 'First line has font key' ); >+ ok( exists $first_line->{'font_size'}, 'First line has font_size key' ); >+ ok( exists $first_line->{'line'}, 'First line has line key' ); >+ >+ if ( scalar @$text_result > 1 ) { >+ cmp_ok( >+ $text_result->[1]{'text_lly'}, '<', $first_line->{'text_lly'}, >+ 'Second line text_lly is less than first (text flows downward)' >+ ); >+ } else { >+ pass('Only one text line returned - skip lly ordering check'); >+ } >+ } >+}; >+ >+# ======================================== >+# Subtest: draw_guide_box() >+# ======================================== >+subtest 'draw_guide_box() returns PDF stream when enabled' => sub { >+ plan tests => 3; >+ >+ my $label_with_box = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ guidebox => 1, >+ llx => 10, >+ lly => 20, >+ width => 100, >+ height => 50, >+ ); >+ my $box_stream = $label_with_box->draw_guide_box(); >+ ok( $box_stream, 'draw_guide_box returns truthy when guidebox enabled' ); >+ like( $box_stream, qr/10 20 100 50 re/, 'PDF stream contains expected rectangle coordinates' ); >+ >+ my $label_no_box = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ guidebox => 0, >+ ); >+ ok( !$label_no_box->draw_guide_box(), 'draw_guide_box returns falsy when guidebox disabled' ); >+}; >+ >+# ======================================== >+# Subtest: barcode() with all supported types >+# ======================================== >+subtest 'barcode() generates all supported barcode types' => sub { >+ plan tests => 7; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 1 ); >+ $pdf->Page(); >+ >+ my @barcode_tests = ( >+ { type => 'CODE39', barcode => 'TEST97531', desc => 'CODE39' }, >+ { type => 'CODE39MOD', barcode => 'TEST97531', desc => 'CODE39MOD (modulo43 checksum)' }, >+ { type => 'CODE39MOD10', barcode => 'TEST97531', desc => 'CODE39MOD10 (modulo10 checksum)' }, >+ { type => 'COOP2OF5', barcode => '97531', desc => 'COOP2OF5 (numeric)' }, >+ { type => 'INDUSTRIAL2OF5', barcode => '97531', desc => 'INDUSTRIAL2OF5 (numeric)' }, >+ { type => 'EAN13', barcode => '5901234123457', desc => 'EAN13 (13-digit)' }, >+ ); >+ >+ for my $bc_test (@barcode_tests) { >+ my $bc_label = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ printing_type => 'BAR', >+ barcode_type => $bc_test->{type}, >+ barcode => $bc_test->{barcode}, >+ ); >+ my $result = eval { $bc_label->create_label(); 1; }; >+ ok( $result, "barcode() $bc_test->{desc} generates without error" ) or diag($@); >+ } >+ >+ # Verify the PDF output has content >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+ ok( -s $out_file, 'PDF with barcodes has content' ); >+}; >+ >+# ======================================== >+# Subtest: create_label() printing type orchestration >+# ======================================== >+subtest 'create_label() printing type orchestration' => sub { >+ plan tests => 4; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 1 ); >+ $pdf->Page(); >+ >+ # BIB - returns label_text (text only) >+ my $label_bib = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ printing_type => 'BIB', >+ ); >+ my $bib_result = $label_bib->create_label(); >+ ok( defined $bib_result, 'BIB printing type returns label text' ); >+ >+ # BAR - returns undef (barcode only) >+ my $label_bar = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ printing_type => 'BAR', >+ barcode_type => 'CODE39', >+ barcode => 'ORCHTEST1', >+ ); >+ my $bar_result = $label_bar->create_label(); >+ ok( !defined $bar_result, 'BAR printing type returns undef (barcode only)' ); >+ >+ # BIBBAR - returns label_text (both text and barcode) >+ my $label_bibbar = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ printing_type => 'BIBBAR', >+ barcode_type => 'CODE39', >+ barcode => 'ORCHTEST2', >+ ); >+ my $bibbar_result = $label_bibbar->create_label(); >+ ok( defined $bibbar_result, 'BIBBAR printing type returns label text' ); >+ >+ # BARBIB - returns label_text (both text and barcode) >+ my $label_barbib = C4::Labels::Label->new( >+ %$label_info, >+ format_string => $format_string, >+ printing_type => 'BARBIB', >+ barcode_type => 'CODE39', >+ barcode => 'ORCHTEST3', >+ ); >+ my $barbib_result = $label_barbib->create_label(); >+ ok( defined $barbib_result, 'BARBIB printing type returns label text' ); >+ >+ # Clean up PDF session >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+}; >+ > $schema->storage->txn_rollback(); > > 1; >diff --git a/t/db_dependent/Patroncards/t_Patroncard.t b/t/db_dependent/Patroncards/t_Patroncard.t >new file mode 100644 >index 0000000000..5f2841884b >--- /dev/null >+++ b/t/db_dependent/Patroncards/t_Patroncard.t >@@ -0,0 +1,387 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Copyright 2026 Koha Development team >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <https://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Test::NoWarnings; >+use Test::More tests => 8; >+use Test::Warn; >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+use File::Temp qw( tempfile ); >+ >+use C4::Creators::PDF; >+use Koha::Database; >+ >+use_ok('C4::Patroncards::Patroncard'); >+ >+my $schema = Koha::Database->new->schema; >+$schema->storage->txn_begin; >+ >+my $builder = t::lib::TestBuilder->new; >+ >+# Build test data >+my $branch = $builder->build( { source => 'Branch' } ); >+my $category = $builder->build( { source => 'Category' } ); >+my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ firstname => 'Testfirst', >+ surname => 'Testsurname', >+ cardnumber => 'CARD12345', >+ branchcode => $branch->{branchcode}, >+ categorycode => $category->{categorycode}, >+ } >+ } >+); >+ >+t::lib::Mocks::mock_userenv( { branchcode => $branch->{branchcode} } ); >+ >+# ---------------------------------------- >+# Helper: build a Patroncard with given layout >+# ---------------------------------------- >+sub build_patroncard { >+ my (%overrides) = @_; >+ my $layout = $overrides{layout} || { >+ units => 'POINT', >+ text => [ >+ '<firstname> <surname>', >+ { llx => 10, lly => 80, font => 'TR', font_size => 10, text_alignment => 'L' }, >+ '<cardnumber>', >+ { llx => 10, lly => 60, font => 'TR', font_size => 8, text_alignment => 'C' }, >+ ], >+ barcode => [ >+ { >+ type => $overrides{barcode_type} || 'CODE39', >+ data => $overrides{barcode_data} || 'CARD12345', >+ llx => 10, >+ lly => 10, >+ height_scale => 0.01, >+ width_scale => 0.8, >+ text_print => '', >+ } >+ ], >+ images => $overrides{images} || {}, >+ }; >+ return C4::Patroncards::Patroncard->new( >+ batch_id => 1, >+ borrower_number => $patron->borrowernumber, >+ llx => 36, >+ lly => 36, >+ height => 200, >+ width => 300, >+ layout => $layout, >+ text_wrap_cols => 40, >+ ); >+} >+ >+# ======================================== >+# Subtest: draw_guide_box() >+# ======================================== >+subtest 'draw_guide_box()' => sub { >+ plan tests => 3; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 0 ); >+ $pdf->Page(); >+ >+ my $card = build_patroncard(); >+ my $result = eval { $card->draw_guide_box($pdf); 1; }; >+ ok( $result, 'draw_guide_box($pdf) succeeds without error' ) or diag($@); >+ >+ # Without $pdf argument should return -1 >+ my $bad_result; >+ warning_like { $bad_result = $card->draw_guide_box(); } >+ qr/No pdf object passed in/, >+ 'draw_guide_box() without $pdf warns appropriately'; >+ is( $bad_result, -1, 'draw_guide_box() without $pdf returns -1' ); >+ >+ # Clean up PDF session >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+}; >+ >+# ======================================== >+# Subtest: draw_guide_grid() >+# ======================================== >+subtest 'draw_guide_grid()' => sub { >+ plan tests => 4; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 0 ); >+ $pdf->Page(); >+ >+ # Test with POINT units >+ # Bug 41718: draw_guide_grid() in Patroncard.pm uses hardcoded "Courier" font >+ # name and calls StrWidth without font/size args, producing warnings >+ my $card_point = build_patroncard(); >+ my $result; >+ { >+ local $SIG{__WARN__} = sub { }; >+ $result = eval { $card_point->draw_guide_grid($pdf); 1; }; >+ } >+ ok( $result, 'draw_guide_grid() with POINT units succeeds' ) or diag($@); >+ >+ # Test with MM units >+ my $card_mm = build_patroncard( >+ layout => { >+ units => 'MM', >+ text => [], >+ barcode => [ >+ { >+ type => 'CODE39', data => 'X', llx => 0, lly => 0, height_scale => 0.01, width_scale => 0.8, >+ text_print => '' >+ } >+ ], >+ images => {}, >+ } >+ ); >+ { >+ local $SIG{__WARN__} = sub { }; >+ $result = eval { $card_mm->draw_guide_grid($pdf); 1; }; >+ } >+ ok( $result, 'draw_guide_grid() with MM units succeeds' ) or diag($@); >+ >+ # Without $pdf argument >+ my $bad_result; >+ warning_like { $bad_result = $card_point->draw_guide_grid(); } >+ qr/No pdf object passed in/, >+ 'draw_guide_grid() without $pdf warns appropriately'; >+ is( $bad_result, -1, 'draw_guide_grid() without $pdf returns -1' ); >+ >+ # Clean up PDF session >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+}; >+ >+# ======================================== >+# Subtest: draw_text() >+# ======================================== >+subtest 'draw_text()' => sub { >+ plan tests => 4; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 0 ); >+ $pdf->Page(); >+ >+ my $card = build_patroncard(); >+ my $result = eval { $card->draw_text($pdf); 1; }; >+ ok( $result, 'draw_text($pdf) succeeds without error' ) or diag($@); >+ >+ # Without $pdf argument >+ my $bad_result; >+ warning_like { $bad_result = $card->draw_text(); } >+ qr/No pdf object passed in/, >+ 'draw_text() without $pdf warns appropriately'; >+ is( $bad_result, -1, 'draw_text() without $pdf returns -1' ); >+ >+ # With empty text layout - should return undef gracefully >+ my $card_no_text = build_patroncard( >+ layout => { >+ units => 'POINT', >+ text => 'not_an_array', >+ barcode => [ >+ { >+ type => 'CODE39', data => 'X', llx => 0, lly => 0, height_scale => 0.01, width_scale => 0.8, >+ text_print => '' >+ } >+ ], >+ images => {}, >+ } >+ ); >+ my $no_text_result = eval { $card_no_text->draw_text($pdf); 1; }; >+ ok( $no_text_result, 'draw_text() with non-ARRAY text layout returns gracefully' ) or diag($@); >+ >+ # Clean up PDF session >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+}; >+ >+# ======================================== >+# Subtest: draw_barcode() >+# ======================================== >+subtest 'draw_barcode()' => sub { >+ plan tests => 3; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 0 ); >+ $pdf->Page(); >+ >+ # CODE39 >+ my $card_c39 = build_patroncard( barcode_type => 'CODE39', barcode_data => 'CARD12345' ); >+ my $result = eval { $card_c39->draw_barcode($pdf); 1; }; >+ ok( $result, 'draw_barcode() CODE39 succeeds' ) or diag($@); >+ >+ # COOP2OF5 >+ my $card_coop = build_patroncard( barcode_type => 'COOP2OF5', barcode_data => '12345' ); >+ $result = eval { $card_coop->draw_barcode($pdf); 1; }; >+ ok( $result, 'draw_barcode() COOP2OF5 succeeds' ) or diag($@); >+ >+ # INDUSTRIAL2OF5 >+ my $card_ind = build_patroncard( barcode_type => 'INDUSTRIAL2OF5', barcode_data => '12345' ); >+ $result = eval { $card_ind->draw_barcode($pdf); 1; }; >+ ok( $result, 'draw_barcode() INDUSTRIAL2OF5 succeeds' ) or diag($@); >+ >+ # Clean up PDF session >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+}; >+ >+# ======================================== >+# Subtest: draw_image() >+# ======================================== >+subtest 'draw_image()' => sub { >+ plan tests => 3; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 0 ); >+ $pdf->Page(); >+ >+ # Create a minimal JPEG using GD >+ my $jpeg_data; >+ eval { require GD; }; >+ if ($@) { >+ >+ # Fallback: minimal JPEG binary >+ $jpeg_data = pack( >+ "H*", >+ "ffd8ffe000104a46494600010100000100010000" >+ . "ffdb004300080606070605080707070909080a0c14" >+ . "0d0c0b0b0c1912130f141d1a1f1e1d1a1c1c2024" >+ . "2e2720222c231c1c2837292c30313434341f27393d" >+ . "38323c2e333432ffc0000b08000100010101011100" >+ . "ffc4001f000001050101010101010000000000000000" >+ . "0102030405060708090a0bffda00080101000003f1" >+ . "00fb52800001ffd9" >+ ); >+ } else { >+ my $im = GD::Image->new( 4, 4, 1 ); >+ my $white = $im->colorAllocate( 255, 255, 255 ); >+ my $red = $im->colorAllocate( 255, 0, 0 ); >+ $im->filledRectangle( 0, 0, 3, 3, $white ); >+ $im->setPixel( 1, 1, $red ); >+ $jpeg_data = $im->jpeg(100); >+ } >+ >+ my $images = { >+ img1 => { >+ data_source => [ { image_source => 'db' } ], >+ data => $jpeg_data, >+ Tx => 5, >+ Ty => 5, >+ Sx => 50, >+ Sy => 50, >+ Ox => 0, >+ Oy => 0, >+ scale => 1, >+ alt => { data => $jpeg_data, Sx => 50, Sy => 50 }, >+ }, >+ }; >+ >+ my $card_img = build_patroncard( images => $images ); >+ my $result; >+ { >+ local $SIG{__WARN__} = sub { }; # suppress possible JPEG color warnings >+ $result = eval { $card_img->draw_image($pdf); 1; }; >+ } >+ ok( $result, 'draw_image() with JPEG data succeeds' ) or diag($@); >+ >+ # Test with image_source => 'none' (should skip) >+ my $images_none = { >+ img1 => { >+ data_source => [ { image_source => 'none' } ], >+ data => $jpeg_data, >+ Tx => 5, >+ Ty => 5, >+ Sx => 50, >+ Sy => 50, >+ Ox => 0, >+ Oy => 0, >+ scale => 1, >+ alt => { data => $jpeg_data, Sx => 50, Sy => 50 }, >+ }, >+ }; >+ my $card_no_img = build_patroncard( images => $images_none ); >+ $result = eval { $card_no_img->draw_image($pdf); 1; }; >+ ok( $result, 'draw_image() with image_source=none skips gracefully' ) or diag($@); >+ >+ # Without $pdf argument >+ my $bad_result; >+ warning_like { $bad_result = $card_img->draw_image(); } >+ qr/No pdf object passed in/, >+ 'draw_image() without $pdf warns appropriately'; >+ >+ # Clean up PDF session >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+}; >+ >+# ======================================== >+# Subtest: End-to-end PDF output >+# ======================================== >+subtest 'End-to-end PDF output' => sub { >+ plan tests => 2; >+ >+ my $pdf = C4::Creators::PDF->new( InitVars => 0 ); >+ $pdf->Page(); >+ >+ my $card = build_patroncard(); >+ eval { >+ $card->draw_guide_box($pdf); >+ $card->draw_text($pdf); >+ $card->draw_barcode($pdf); >+ }; >+ >+ my ( $out_fh, $out_file ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); >+ open( $out_fh, '>', $out_file ); >+ select $out_fh; >+ eval { $pdf->End(); }; >+ select STDOUT; >+ close $out_fh; >+ >+ ok( -s $out_file, 'End-to-end PDF file has content' ); >+ >+ # Verify PDF header >+ open( my $check_fh, '<', $out_file ) or die "Cannot open $out_file: $!"; >+ my $header; >+ read( $check_fh, $header, 5 ); >+ close $check_fh; >+ is( $header, '%PDF-', 'Output file has valid PDF header' ); >+}; >+ >+$schema->storage->txn_rollback(); >+ >+1; >-- >2.43.0 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 41719
: 192076