Fix false-positive failures in "has hits" search tests
The search tests sometimes reported failure (and fired notifications) even
though the result page showed matches. Two changes:
- The per-search mocha timeout was a hard 20s, shorter than search()'s own
internal waits, so a slow total-count computation could trip it before the
count was read. The timeout is now configurable via KORAP_SEARCH_TIMEOUT
(default 60s).
- search() no longer relies on a fixed 2s sleep. It waits until the result
page has settled -- the total is shown, matches are listed, or a "no
matches" message appears -- and reads the outcome immediately.
Note this slightly relaxes what "has hits" verifies: when glimpse/cutoff is
on, Kalamar lists matches without computing a total, so the test now accepts
the presence of listed matches as proof of hits rather than requiring a
counted total. This keeps the check stable across glimpse states; when
glimpse is off the exact total is still read and asserted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change-Id: I92f2004ba668722939e3a70e3ab8aa7eae698b4d
diff --git a/Readme.md b/Readme.md
index d492bbe..7af1131 100644
--- a/Readme.md
+++ b/Readme.md
@@ -31,6 +31,7 @@
| `KORAP_PASSWORD` | `password2` | Password for KorAP login (also accepts legacy `KORAP_PWD`) |
| `KORAP_QUERIES` | `geht, [orth=geht & cmc/pos=VVFIN]` | Comma-separated list of queries to test |
| `KORAP_MIN_TOKENS_IN_CORPUS` | `100000` | Minimum expected number of tokens for corpus statistics test |
+| `KORAP_SEARCH_TIMEOUT` | `60000` | Per-query timeout in ms for the "has hits" search tests. Raise it for slow/complex queries on very large corpora to avoid false-positive timeout failures |
| `KORAP_HEADLESS` | `true` | Set to `false` or `0` to run browser in UI mode (visible window) instead of headless |
| `SLACK_WEBHOOK_URL` | _(none)_ | Slack webhook URL for test failure notifications (text only) |
| `SLACK_TOKEN` | _(none)_ | Slack bot token for uploading failure screenshots |
diff --git a/lib/korap_rc.js b/lib/korap_rc.js
index 158ecbb..0bdf1af 100644
--- a/lib/korap_rc.js
+++ b/lib/korap_rc.js
@@ -82,82 +82,37 @@
await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
- // Wait for search results to be fully loaded
- try {
- await page.waitForSelector('ol li, #resultinfo, .result-item', {
- visible: true,
- timeout: 15000
- });
- // Give additional time for the results count to be populated
- await new Promise(resolve => setTimeout(resolve, 2000));
- } catch (error) {
- // Continue if timeout, fallback methods will handle it
- }
+ // Wait until the results page has actually settled, then read the
+ // count immediately (no fixed sleep). Kalamar renders the precise
+ // total into #total-results (e.g. "1,039,193") on hits, but when
+ // glimpse/cutoff is on it lists matches without computing a total,
+ // and on a miss it shows a .no-results message. A search "has hits"
+ // as soon as any match is listed, so settle on whichever appears.
+ await page.waitForFunction(() => {
+ const total = document.querySelector('#total-results');
+ if (total && /\d/.test(total.textContent || '')) return true;
+ if (document.querySelectorAll('#search ol li').length > 0) return true;
+ return document.querySelector('#search .no-results, .no-results') !== null;
+ }, { timeout: 15000, polling: 200 });
- const resultsInfo = await page.evaluate(() => {
- // Check common selectors for result counts
- const selectors = [
- '#total-results',
- '#resultinfo',
- '.result-count',
- '.total-results',
- '[data-results]',
- '.found'
- ];
-
- for (const selector of selectors) {
- const element = document.querySelector(selector);
- if (element) {
- const text = element.textContent || element.innerText || '';
- const numbers = text.match(/\d+/g);
- if (numbers && numbers.length > 0) {
- return {
- selector: selector,
- numbers: numbers
- };
- }
- }
+ const hits = await page.evaluate(() => {
+ const total = document.querySelector('#total-results');
+ if (total) {
+ // The span holds only the number; strip thousands separators
+ // (commas, periods, spaces) and parse what remains.
+ const digits = (total.textContent || '').replace(/[^\d]/g, '');
+ if (digits.length > 0) return parseInt(digits, 10);
}
- // Look in the page title for results count
- const title = document.title;
- if (title) {
- const numbers = title.match(/\d+/g);
- if (numbers && numbers.length > 0) {
- return {
- selector: 'title',
- numbers: numbers
- };
- }
- }
+ // No exact total (e.g. glimpse/cutoff on): listed matches still
+ // prove the query has hits, so fall back to counting them.
+ const items = document.querySelectorAll('#search ol li').length;
+ if (items > 0) return items;
- // Count the actual result items as fallback
- const resultItems = document.querySelectorAll('ol li');
- if (resultItems.length > 0) {
- return {
- selector: 'counted-items',
- numbers: [resultItems.length.toString()]
- };
- }
-
- return null;
+ // Explicit "no matches" state reported by Kalamar.
+ return 0;
});
- if (!resultsInfo || !resultsInfo.numbers || resultsInfo.numbers.length === 0) {
- // Final fallback: just count visible list items
- const itemCount = await page.evaluate(() => {
- return document.querySelectorAll('ol li').length;
- });
-
- if (itemCount > 0) {
- return itemCount;
- }
-
- throw new Error("Cannot find any results count on the page");
- }
-
- // Extract the largest number found (likely the total results)
- const hits = Math.max(...resultsInfo.numbers.map(n => parseInt(n, 10)));
return hits;
} catch (error) {
throw new Error(`Failed to perform search: ${error.message}`);
diff --git a/test/korap-ui.js b/test/korap-ui.js
index 621b22f..bec14bd 100644
--- a/test/korap-ui.js
+++ b/test/korap-ui.js
@@ -20,6 +20,7 @@
const KORAP_PWD = process.env.KORAP_PWD || process.env.KORAP_PASSWORD || "password2";
const KORAP_QUERIES = process.env.KORAP_QUERIES || 'geht, [orth=geht & cmc/pos=VVFIN]'
const KORAP_MIN_TOKENS_IN_CORPUS = parseInt(process.env.KORAP_MIN_TOKENS_IN_CORPUS || "100000", 10);
+const KORAP_SEARCH_TIMEOUT = parseInt(process.env.KORAP_SEARCH_TIMEOUT || "60000", 10);
const NOTIFY_ON_SUCCESS = process.env.NOTIFY_ON_SUCCESS === 'true' || process.env.NOTIFY_ON_SUCCESS === '1';
const KORAP_HEADLESS = !(process.env.KORAP_HEADLESS === 'false' || process.env.KORAP_HEADLESS === '0');
const korap_rc = require('../lib/korap_rc.js').new(KORAP_URL)
@@ -339,7 +340,7 @@
await korap_rc.assure_glimpse_off(page)
const hits = await korap_rc.search(page, query)
hits.should.be.above(0)
- })).timeout(20000)
+ })).timeout(KORAP_SEARCH_TIMEOUT)
})
})