Adds test for configurable hint foundries (See #10306)

Change-Id: Ib193d7c053e37e214f4f2be288fe079df744f2d4
diff --git a/Changes b/Changes
index 85ff5e7..fcf47d5 100644
--- a/Changes
+++ b/Changes
@@ -4,12 +4,13 @@
         - Enhance handling of improper json files (diewald)
         - Support API version 1.1 (diewald)
         - Support title change in button groups (diewald)
-        - Eliminates authorized_only (deprecation in API version 1.1)
+        - Eliminates authorized_only (deprecation in API version 1.1) (hebasta)
         - Add 'glimpse' option to query panel (diewald)
         - Fix test suite for glimpse-rearrangement (diewald)
         - Mark "notinindex"-fields (diewald)
         - Fix test suite for "allow-same-origin" sandbox rule (diewald)
         - Escape commas and asterisks in query-by-match query creator (kupietz, hebasta)
+        - Tests for configurable hint foundries added (hebasta)
 
 0.64 2026-02-14
         - Improve 'Plugins' mounting (diewald)
diff --git a/dev/js/spec/hintSpec.js b/dev/js/spec/hintSpec.js
index 3b07bd9..31f48ee 100644
--- a/dev/js/spec/hintSpec.js
+++ b/dev/js/spec/hintSpec.js
@@ -1,6 +1,6 @@
 "use strict";
 
-define(['hint', 'hint/input', 'hint/contextanalyzer', 'hint/menu', 'hint/item'], function (hintClass, inputClass, contextClass, menuClass, menuItemClass) {
+define(['hint', 'hint/input', 'hint/contextanalyzer', 'hint/menu', 'hint/item', 'hint/foundries'], function (hintClass, inputClass, contextClass, menuClass, menuItemClass, foundriesClass) {
 
   function emitKeyboardEvent (element, type, letter, keyCode) {
     // event type : keydown, keyup, keypress
@@ -67,6 +67,7 @@
         };
       };
     };
+      
     KorAP.API.getMatchInfo = undefined;
     KorAP.context = undefined;
     // KorAP.annotationHelper = undefined;
@@ -227,7 +228,7 @@
       expect(hint).toBeTruthy();
     });
 
-    
+
     it('should alert at char pos', function () {
       var hint = hintClass.create({
         inputField : input
@@ -509,7 +510,64 @@
       expect(menu.container().element().classList.contains("visible")).toBeFalsy();
     });
 
+    it('should filter available foundries based on configuration', function () {
+  
+      KorAP.annotationHelper["-"] = [
+        ["Base Annotation", "base/s=", "Structure"],
+        ["CoreNLP", "corenlp/", "Constituency, Named Entities, Part-of-Speech"],
+        ["TreeTagger", "tt/", "Lemma, Part-of-Speech"]
+      ];
+      KorAP.annotationHelper.filterByConfig();
+      let foundries = KorAP.annotationHelper["-"].map(e => e[1]);
+      expect(foundries).toContain("base/s=");
+      expect(foundries).toContain("corenlp/");
+      expect(foundries).toContain("tt/");
     
+    
+      //set configuration to corenlp and tt
+      document.body.setAttribute('data-hint-foundries', 'corenlp,tt');
+      KorAP.annotationHelper.filterByConfig();
+      foundries = KorAP.annotationHelper["-"].map(e => e[1]);
+      expect(foundries).not.toContain("base/s=");
+      expect(foundries).toContain("corenlp/");
+      expect(foundries).toContain("tt/");
+      
+      //set configuration to corenlp only, with different case and whitespaces
+      document.body.setAttribute('data-hint-foundries', ' coREnlp');
+      KorAP.annotationHelper.filterByConfig();
+      foundries = KorAP.annotationHelper["-"].map(e => e[1]);
+      expect(foundries).not.toContain("base/s=");
+      expect(foundries).toContain("corenlp/");
+      expect(foundries).not.toContain("tt/");
+      
+      //Clean up 
+      document.body.removeAttribute('data-hint-foundries');
+    });
+
+    it('should apply configured foundry filter if initialized', function () {
+    
+      KorAP.annotationHelper["-"] = [
+        ["Base Annotation", "base/s=", "Structure"],
+        ["CoreNLP", "corenlp/", "Constituency, Named Entities, Part-of-Speech"],
+        ["TreeTagger", "tt/", "Lemma, Part-of-Speech"]
+      ];
+
+      document.body.setAttribute('data-hint-foundries', 'tt');
+
+      var hint = hintClass.create({
+        inputField : input
+      });
+      expect(hint).toBeTruthy();
+           
+      let foundries = KorAP.annotationHelper["-"].map(e => e[1]);
+      expect(foundries).not.toContain("base/s=");
+      expect(foundries).not.toContain("corenlp/");
+      expect(foundries).toContain("tt/");
+
+      //Clean up 
+      document.body.removeAttribute('data-hint-foundries');
+    });
+
     xit('should remove all menus on escape');
   });
 
diff --git a/t/hint_foundries.t b/t/hint_foundries.t
new file mode 100644
index 0000000..e427bdf
--- /dev/null
+++ b/t/hint_foundries.t
@@ -0,0 +1,165 @@
+use Mojo::Base -strict;
+use Test::More;
+use Test::Mojo;
+
+# Test 1: Default foundries (no configuration)
+subtest 'Default foundries' => sub {
+  my $t = Test::Mojo->new('Kalamar');
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  is_deeply(
+    $defaults,
+    [qw(base corenlp dereko malt marmot opennlp spacy tt)],
+    'Default foundries are set correctly'
+  );
+  
+  # Check that data-hint-foundries is rendered in HTML
+  $t->get_ok('/')
+    ->status_is(200)
+    ->attr_like('body', 'data-hint-foundries', qr/base/)
+    ->attr_like('body', 'data-hint-foundries', qr/corenlp/)
+    ->attr_like('body', 'data-hint-foundries', qr/spacy/);
+};
+
+
+# Test 2: Custom foundries via config (inclusions only)
+subtest 'Custom foundries via config' => sub {
+  my $t = Test::Mojo->new('Kalamar' => {
+    Kalamar => {
+      hint_foundries => ['base', 'marmot', 'tt']
+    }
+  });
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  is_deeply(
+    $defaults,
+    ['base', 'marmot', 'tt'],
+    'Custom foundries are set correctly'
+  );
+  
+  $t->get_ok('/')
+    ->status_is(200)
+    ->attr_like('body', 'data-hint-foundries', qr/base/)
+    ->attr_like('body', 'data-hint-foundries', qr/marmot/)
+    ->attr_like('body', 'data-hint-foundries', qr/tt/)
+    ->attr_unlike('body', 'data-hint-foundries', qr/corenlp/)
+    ->attr_unlike('body', 'data-hint-foundries', qr/spacy/);
+};
+
+
+# Test 3: Exclusions via config (e.g., -spacy, -corenlp)
+subtest 'Exclusions via config' => sub {
+  my $t = Test::Mojo->new('Kalamar' => {
+    Kalamar => {
+      hint_foundries => ['-spacy', '-corenlp']
+    }
+  });
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  
+  # Should contain all defaults except spacy and corenlp
+  ok((grep { $_ eq 'base' } @$defaults), 'Contains base');
+  ok((grep { $_ eq 'dereko' } @$defaults), 'Contains dereko');
+  ok((grep { $_ eq 'malt' } @$defaults), 'Contains malt');
+  ok((grep { $_ eq 'marmot' } @$defaults), 'Contains marmot');
+  ok((grep { $_ eq 'opennlp' } @$defaults), 'Contains opennlp');
+  ok((grep { $_ eq 'tt' } @$defaults), 'Contains tt');
+  ok(!(grep { $_ eq 'spacy' } @$defaults), 'Does not contain spacy');
+  ok(!(grep { $_ eq 'corenlp' } @$defaults), 'Does not contain corenlp');
+  
+  $t->get_ok('/')
+    ->status_is(200)
+    ->attr_like('body', 'data-hint-foundries', qr/base/)
+    ->attr_unlike('body', 'data-hint-foundries', qr/spacy/)
+    ->attr_unlike('body', 'data-hint-foundries', qr/corenlp/);
+};
+
+
+# Test 4: Environment variable KALAMAR_HINT_FOUNDRIES
+subtest 'Environment variable' => sub {
+  local $ENV{KALAMAR_HINT_FOUNDRIES} = 'base,tt,marmot';
+  
+  my $t = Test::Mojo->new('Kalamar');
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  is_deeply(
+    $defaults,
+    ['base', 'tt', 'marmot'],
+    'Foundries from environment variable'
+  );
+  
+  $t->get_ok('/')
+    ->status_is(200)
+    ->attr_like('body', 'data-hint-foundries', qr/base,tt,marmot/);
+};
+
+
+# Test 5: Environment variable with exclusions
+subtest 'Environment variable with exclusions' => sub {
+  local $ENV{KALAMAR_HINT_FOUNDRIES} = '-spacy,-opennlp';
+  
+  my $t = Test::Mojo->new('Kalamar');
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  
+  ok((grep { $_ eq 'base' } @$defaults), 'Contains base');
+  ok((grep { $_ eq 'corenlp' } @$defaults), 'Contains corenlp');
+  ok(!(grep { $_ eq 'spacy' } @$defaults), 'Does not contain spacy');
+  ok(!(grep { $_ eq 'opennlp' } @$defaults), 'Does not contain opennlp');
+};
+
+
+# Test 6: Environment variable overrides config
+subtest 'Environment variable overrides config' => sub {
+  local $ENV{KALAMAR_HINT_FOUNDRIES} = 'base,tt';
+  
+  my $t = Test::Mojo->new('Kalamar' => {
+    Kalamar => {
+      hint_foundries => ['corenlp', 'marmot', 'spacy']
+    }
+  });
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  is_deeply(
+    $defaults,
+    ['base', 'tt'],
+    'Environment variable takes precedence over config'
+  );
+};
+
+
+# Test 7: Empty config foundries array uses defaults
+subtest 'Empty config uses defaults' => sub {
+  my $t = Test::Mojo->new('Kalamar' => {
+    Kalamar => {
+      hint_foundries => []
+    }
+  });
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  is_deeply(
+    $defaults,
+    [qw(base corenlp dereko malt marmot opennlp spacy tt)],
+    'Empty array results in defaults'
+  );
+};
+
+
+# Test 8: Case insensitivity of exclusions
+subtest 'Case insensitive exclusions' => sub {
+  my $t = Test::Mojo->new('Kalamar' => {
+    Kalamar => {
+      hint_foundries => ['-SPACY', '-CoreNLP']
+    }
+  });
+  
+  my $defaults = $t->app->defaults('hint_foundries');
+  
+  ok(!(grep { lc($_) eq 'spacy' } @$defaults), 'Does not contain spacy (case insensitive)');
+  ok(!(grep { lc($_) eq 'corenlp' } @$defaults), 'Does not contain corenlp (case insensitive)');
+  ok((grep { $_ eq 'base' } @$defaults), 'Still contains base');
+};
+
+
+done_testing;
+__END__