From 70d04446f35f624e114806e2958954edb9a35cde Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 29 Aug 2026 19:11:54 -0400 Subject: [PATCH 1/4] Bug 681635 - Sort and de-duplicate merged product values Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- js/productform.js | 63 +++++++++---------------------- t/015productform.t | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 46 deletions(-) create mode 100644 t/015productform.t diff --git a/js/productform.js b/js/productform.js index dd5fa676a9..b467198b8e 100644 --- a/js/productform.js +++ b/js/productform.js @@ -329,53 +329,24 @@ function fake_diff_array(a, b) { * @return Merged and sorted array. */ function merge_arrays(a, b, b_is_select) { - var pos_a = 0; - var pos_b = 0; + var items = a.slice(); var ret = new Array(); - var bitem, aitem; - - // Iterate through both arrays and add the larger item to the return - // list. Remove dupes, too. Use toLowerCase to provide - // case-insensitivity. - while ((pos_a < a.length) && (pos_b < b.length)) { - aitem = a[pos_a]; - if (b_is_select) - bitem = b[pos_b].value; - else - bitem = b[pos_b]; - - // Smaller item in list a. - if (aitem.toLowerCase() < bitem.toLowerCase()) { - ret[ret.length] = aitem; - pos_a++; - } - else { - // Smaller item in list b. - if (aitem.toLowerCase() > bitem.toLowerCase()) { - ret[ret.length] = bitem; - pos_b++; - } - else { - // List contents are equal, include both counters. - ret[ret.length] = aitem; - pos_a++; - pos_b++; - } - } - } - - // Catch leftovers here. These sections are ugly code-copying. - if (pos_a < a.length) - for (; pos_a < a.length ; pos_a++) - ret[ret.length] = a[pos_a]; - - if (pos_b < b.length) { - for (; pos_b < b.length; pos_b++) { - if (b_is_select) - bitem = b[pos_b].value; - else - bitem = b[pos_b]; - ret[ret.length] = bitem; + var i; + + for (i = 0; i < b.length; i++) + items[items.length] = b_is_select ? b[i].value : b[i]; + + items.sort(function(left, right) { + left = left.toLowerCase(); + right = right.toLowerCase(); + return left < right ? -1 : left > right ? 1 : 0; + }); + + for (i = 0; i < items.length; i++) { + if (!ret.length + || items[i].toLowerCase() != ret[ret.length - 1].toLowerCase()) + { + ret[ret.length] = items[i]; } } diff --git a/t/015productform.t b/t/015productform.t new file mode 100644 index 0000000000..a4a329fd76 --- /dev/null +++ b/t/015productform.t @@ -0,0 +1,94 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + + +################## +#Bugzilla Test 15# +####Productform### + +use 5.14.0; +use strict; +use warnings; + +use File::Spec; +use Test::More; + +my $node; +foreach my $dir (File::Spec->path()) { + foreach my $name (qw(node node.exe)) { + my $path = File::Spec->catfile($dir, $name); + if (-x $path && !-d $path) { + $node = $path; + last; + } + } + last if $node; +} + +if (!$node) { + plan tests => 1; + fail('Node.js is required to test js/productform.js'); + exit; +} + +my $script = <<'JS'; +var fs = require('fs'); +var vm = require('vm'); + +vm.runInThisContext( + fs.readFileSync('js/productform.js', 'utf8'), + { filename: 'js/productform.js' } +); + +console.log( + merge_arrays( + ['Trunk', '2.0'], + ['unspecified', 'Trunk'], + false + ).join('\t') +); +console.log( + merge_arrays( + ['Beta', 'alpha'], + [{ value: 'ALPHA' }, { value: 'Release' }], + true + ).join('\t') +); +JS + +my $pid = open(my $fh, '-|', $node, '-e', $script); +if (!defined $pid) { + plan tests => 1; + fail("could not run $node: $!"); + exit; +} + +my @results = <$fh>; +close($fh); +my $status = $?; + +if ($status || @results != 2) { + plan tests => 1; + fail('js/productform.js did not produce the expected test output'); + diag("Node.js exit status: $status"); + diag("Node.js output:\n" . join('', @results)); + exit; +} + +chomp(@results); +plan tests => 2; + +is_deeply( + [split(/\t/, $results[0])], + ['2.0', 'Trunk', 'unspecified'], + 'unsorted product values are sorted and de-duplicated' +); +is_deeply( + [split(/\t/, $results[1])], + ['alpha', 'Beta', 'Release'], + 'select options are merged, sorted, and de-duplicated' +); From 7fc21803fa1dc24d112a9eeca68bd3089c8f10f1 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 29 Aug 2026 19:26:21 -0400 Subject: [PATCH 2/4] Bug 681635 - Install Node.js in the Perl test image Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docker/images/Dockerfile.perl-testsuite | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/images/Dockerfile.perl-testsuite b/docker/images/Dockerfile.perl-testsuite index a5ff0ede0f..b1a620d27f 100644 --- a/docker/images/Dockerfile.perl-testsuite +++ b/docker/images/Dockerfile.perl-testsuite @@ -19,6 +19,7 @@ RUN apt-get update && apt-get -y dist-upgrade && \ cpanminus \ mariadb-client \ netcat-traditional \ + nodejs \ build-essential \ libapache2-mod-perl2 \ libapache2-mod-perl2-dev \ From 66b575ef8379ed4e5152b04fd36347270d2db17a Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 29 Aug 2026 19:40:54 -0400 Subject: [PATCH 3/4] Bug 681635 - Skip productform tests without Node.js Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- t/015productform.t | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/t/015productform.t b/t/015productform.t index a4a329fd76..07c5b54ccd 100644 --- a/t/015productform.t +++ b/t/015productform.t @@ -29,11 +29,7 @@ foreach my $dir (File::Spec->path()) { last if $node; } -if (!$node) { - plan tests => 1; - fail('Node.js is required to test js/productform.js'); - exit; -} +plan skip_all => 'Node.js is required to test js/productform.js' if !$node; my $script = <<'JS'; var fs = require('fs'); From d1f650e8e7bbf6de9f341704f08c6442600954a2 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 29 Aug 2026 19:59:36 -0400 Subject: [PATCH 4/4] Bug 681635 - Require Node.js in the release test image Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docker/images/Dockerfile.perl-testsuite | 2 ++ t/015productform.t | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docker/images/Dockerfile.perl-testsuite b/docker/images/Dockerfile.perl-testsuite index b1a620d27f..7e1cf821c4 100644 --- a/docker/images/Dockerfile.perl-testsuite +++ b/docker/images/Dockerfile.perl-testsuite @@ -10,6 +10,8 @@ ARG BZDB="" FROM bugzilla/bugzilla-perl-slim${BZDB}:20250925.1 +ENV BZ_REQUIRE_NODE=1 + WORKDIR /app # Install system dependencies diff --git a/t/015productform.t b/t/015productform.t index 07c5b54ccd..fe9621f346 100644 --- a/t/015productform.t +++ b/t/015productform.t @@ -19,7 +19,7 @@ use Test::More; my $node; foreach my $dir (File::Spec->path()) { - foreach my $name (qw(node node.exe)) { + foreach my $name (qw(node nodejs node.exe)) { my $path = File::Spec->catfile($dir, $name); if (-x $path && !-d $path) { $node = $path; @@ -29,6 +29,12 @@ foreach my $dir (File::Spec->path()) { last if $node; } +if (!$node && $ENV{BZ_REQUIRE_NODE}) { + plan tests => 1; + fail('Node.js is required to test js/productform.js'); + exit; +} + plan skip_all => 'Node.js is required to test js/productform.js' if !$node; my $script = <<'JS';