blob: e2d8a15b60ec77a3f424b5c4c95da7cdeaffb2ff [file] [log] [blame]
Marc Kupietz55fc3162022-12-04 16:25:49 +01001const chai = require('chai');
2const assert = chai.assert;
Marc Kupietz5e45a2f2022-12-03 15:32:40 +01003
4class KorAPRC {
5 korap_url = ""
6
7 constructor(korap_url) {
8 this.korap_url = korap_url
9 }
10
11 static new(korap_url) {
12 return new KorAPRC(korap_url)
13 }
14
15 async login(page, username, password) {
Marc Kupietz93d7f702025-06-27 15:41:48 +020016 try {
17 await page.goto(this.korap_url, { waitUntil: 'domcontentloaded' });
18 if (username == "") return false;
19 if (password == "") return false;
Marc Kupietz5e45a2f2022-12-03 15:32:40 +010020
Marc Kupietz96aa4152026-06-12 17:20:32 +020021 // Check if already logged in as the correct user
22 const currentLoggedInUser = await page.evaluate(() => {
23 const profileBtn = document.querySelector('.dropdown-btn.profile');
24 if (profileBtn) {
25 const userNameEl = profileBtn.querySelector('.user-name');
26 return userNameEl ? userNameEl.textContent.trim() : null;
27 }
28 return null;
29 });
30
31 if (currentLoggedInUser === username) {
32 console.log(`Already logged in as ${username}`);
33 return true;
34 } else if (currentLoggedInUser) {
35 console.log(`Logged in as different user: ${currentLoggedInUser}. Logging out...`);
36 await this.logout(page);
37 await page.goto(this.korap_url, { waitUntil: 'domcontentloaded' });
38 }
39
Marc Kupietz93d7f702025-06-27 15:41:48 +020040 await page.waitForSelector('.dropdown-btn', { visible: true });
Marc Kupietz96aa4152026-06-12 17:20:32 +020041 const loginBtn = await page.$('.dropdown-btn.login') || await page.$('.dropdown-btn');
42 if (!loginBtn) {
43 return false;
44 }
45 await loginBtn.click();
Marc Kupietz93d7f702025-06-27 15:41:48 +020046 await page.waitForSelector('input[name=handle_or_email]', { visible: true });
47 const username_field = await page.$("input[name=handle_or_email]")
48 if (username_field != null) {
49 await username_field.focus();
50 await username_field.type(username);
51 const password_field = await page.$("input[name=pwd]")
52 await password_field.focus()
53 await page.keyboard.type(password)
54 await page.keyboard.press("Enter")
55 } else {
56 return false
57 }
58
59 await page.waitForNavigation({ waitUntil: 'domcontentloaded' }); // Wait for navigation after login
60 await page.waitForSelector("#q-field", { visible: true }); // Wait for query field to confirm login
Marc Kupietz96aa4152026-06-12 17:20:32 +020061 const logout = await page.$(".logout, a[data-testid=\"logout\"], .dropdown-btn.profile")
Marc Kupietz93d7f702025-06-27 15:41:48 +020062 if (logout == null) {
63 return false
64 }
65
66 return true
67 } catch (error) {
68 console.error(`Login failed: ${error.message}`);
69 return false;
Marc Kupietz5e45a2f2022-12-03 15:32:40 +010070 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +010071 }
72
Marc Kupietz6329cac2026-06-15 16:39:55 +020073 async search(page, query, options = {}) {
74 // Both the navigation and the post-navigation settle should honour the
75 // caller-supplied timeout. Without this, Puppeteer's default 30s
76 // navigation timeout silently caps the wait, so complex queries on very
77 // large corpora time out (false-positive failure) even when
78 // KORAP_SEARCH_TIMEOUT is raised. Default to 30s for backwards compat.
79 const timeout = options.timeout || 30000;
80 // Optional virtual corpus restriction (e.g. "pubDate in 2020"). When
81 // set, it is passed as the corpus query (cq) to narrow the search and
82 // keep it fast enough to finish within the timeout.
83 const vc = options.vc || "";
Marc Kupietz964e7772025-06-03 15:02:30 +020084 try {
Marc Kupietz6329cac2026-06-15 16:39:55 +020085 if (vc) {
86 // A VC can't be entered through the query field, so navigate to
87 // the search URL directly with q + cq. The session cookie is
88 // preserved, so the user stays logged in.
89 const url = new URL(this.korap_url);
90 url.searchParams.set('q', query);
91 url.searchParams.set('ql', 'poliqarp');
92 url.searchParams.set('cq', vc);
93 await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout });
94 } else {
95 await page.waitForSelector("#q-field", { visible: true });
96 const query_field = await page.$("#q-field");
97 assert.notEqual(query_field, null, "Query field not found");
Marc Kupietz964e7772025-06-03 15:02:30 +020098
Marc Kupietz6329cac2026-06-15 16:39:55 +020099 await query_field.click({ clickCount: 3 });
100 await page.keyboard.type(query);
101 await page.keyboard.press("Enter");
Marc Kupietz964e7772025-06-03 15:02:30 +0200102
Marc Kupietz6329cac2026-06-15 16:39:55 +0200103 await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout });
104 }
Marc Kupietz93d7f702025-06-27 15:41:48 +0200105
Marc Kupietzf0079692026-06-14 20:25:54 +0200106 // Wait until the results page has actually settled, then read the
107 // count immediately (no fixed sleep). Kalamar renders the precise
108 // total into #total-results (e.g. "1,039,193") on hits, but when
109 // glimpse/cutoff is on it lists matches without computing a total,
110 // and on a miss it shows a .no-results message. A search "has hits"
111 // as soon as any match is listed, so settle on whichever appears.
112 await page.waitForFunction(() => {
113 const total = document.querySelector('#total-results');
114 if (total && /\d/.test(total.textContent || '')) return true;
115 if (document.querySelectorAll('#search ol li').length > 0) return true;
116 return document.querySelector('#search .no-results, .no-results') !== null;
Marc Kupietz6329cac2026-06-15 16:39:55 +0200117 }, { timeout, polling: 200 });
Marc Kupietz93d7f702025-06-27 15:41:48 +0200118
Marc Kupietzf0079692026-06-14 20:25:54 +0200119 const hits = await page.evaluate(() => {
120 const total = document.querySelector('#total-results');
121 if (total) {
122 // The span holds only the number; strip thousands separators
123 // (commas, periods, spaces) and parse what remains.
124 const digits = (total.textContent || '').replace(/[^\d]/g, '');
125 if (digits.length > 0) return parseInt(digits, 10);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200126 }
127
Marc Kupietzf0079692026-06-14 20:25:54 +0200128 // No exact total (e.g. glimpse/cutoff on): listed matches still
129 // prove the query has hits, so fall back to counting them.
130 const items = document.querySelectorAll('#search ol li').length;
131 if (items > 0) return items;
Marc Kupietz964e7772025-06-03 15:02:30 +0200132
Marc Kupietzf0079692026-06-14 20:25:54 +0200133 // Explicit "no matches" state reported by Kalamar.
134 return 0;
Marc Kupietz964e7772025-06-03 15:02:30 +0200135 });
136
Marc Kupietz93d7f702025-06-27 15:41:48 +0200137 return hits;
138 } catch (error) {
139 throw new Error(`Failed to perform search: ${error.message}`);
Marc Kupietz964e7772025-06-03 15:02:30 +0200140 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100141 }
142
143 async logout(page) {
Marc Kupietz964e7772025-06-03 15:02:30 +0200144 try {
Marc Kupietz96aa4152026-06-12 17:20:32 +0200145 // First, try to find the logout link in the page
146 const logoutHref = await page.evaluate(() => {
147 const logoutEl = document.querySelector('a.logout, a[data-testid="logout"]');
148 return logoutEl ? logoutEl.getAttribute('href') : null;
149 });
Marc Kupietz964e7772025-06-03 15:02:30 +0200150
Marc Kupietz96aa4152026-06-12 17:20:32 +0200151 if (logoutHref) {
152 const absoluteLogoutUrl = new URL(logoutHref, page.url()).href;
153 await page.goto(absoluteLogoutUrl, { waitUntil: 'domcontentloaded', timeout: 10000 });
154 } else {
155 const currentUrl = await page.url();
156 const baseUrl = currentUrl.replace(/\/$/, '');
157 try {
158 await page.goto(baseUrl + '/user/logout', { waitUntil: 'domcontentloaded', timeout: 5000 });
159 } catch (e) {
160 await page.goto(baseUrl + '/logout', { waitUntil: 'domcontentloaded', timeout: 5000 });
161 }
162 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200163
164 // Navigate back to main page to ensure clean state for subsequent tests
165 await page.goto(this.korap_url, { waitUntil: 'domcontentloaded', timeout: 10000 });
166
Marc Kupietz96aa4152026-06-12 17:20:32 +0200167 // Verify we are actually logged out
168 const loggedOut = await page.evaluate(() => {
169 return document.querySelector('a.logout, a[data-testid="logout"], .dropdown-btn.profile') === null;
170 });
171
172 return loggedOut;
Marc Kupietz964e7772025-06-03 15:02:30 +0200173 } catch (error) {
Marc Kupietz96aa4152026-06-12 17:20:32 +0200174 console.error(`Logout failed: ${error.message}`);
Marc Kupietz964e7772025-06-03 15:02:30 +0200175 return false;
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100176 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100177 }
178
179 async assure_glimpse_off(page) {
Marc Kupietz0462c7d2026-03-21 10:10:00 +0100180 // Get the cutoff checkbox - works in both old and new Kalamar versions
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100181 const glimpse = await page.$("input[name=cutoff]")
Marc Kupietz0462c7d2026-03-21 10:10:00 +0100182 if (!glimpse) {
183 console.log("Glimpse checkbox not found, skipping")
184 return
185 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100186 const glimpse_value = await (await glimpse.getProperty('checked')).jsonValue()
187 if (glimpse_value) {
Marc Kupietz0462c7d2026-03-21 10:10:00 +0100188 // Try new Kalamar version first (toggle button with class 'glimpse')
189 const newGlimpseButton = await page.$(".glimpse")
190 if (newGlimpseButton) {
191 const isVisible = await page.evaluate(el => {
192 const style = window.getComputedStyle(el)
193 return style.display !== 'none' && style.visibility !== 'hidden'
194 }, newGlimpseButton)
195 if (isVisible) {
196 await newGlimpseButton.click()
197 return
198 }
199 }
200 // Fall back to old Kalamar version (label with id 'glimpse')
201 const oldGlimpseLabel = await page.$("#glimpse")
202 if (oldGlimpseLabel) {
203 const isVisible = await page.evaluate(el => {
204 const style = window.getComputedStyle(el.parentNode || el)
205 return style.display !== 'none' && style.visibility !== 'hidden'
206 }, oldGlimpseLabel)
207 if (isVisible) {
208 await page.click("#glimpse")
209 return
210 }
211 }
212 // Last resort: directly toggle the checkbox via JavaScript
213 await page.evaluate(() => {
214 const checkbox = document.querySelector("input[name=cutoff]")
215 if (checkbox) checkbox.checked = false
216 })
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100217 }
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200218 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100219
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200220 async check_corpus_statistics(page, minTokenThreshold = 1000) {
221 try {
Marc Kupietz669c0432025-07-12 12:33:42 +0200222 console.log(`Starting corpus statistics check with minTokenThreshold: ${minTokenThreshold}`);
223
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200224 // Navigate to the corpus view if not already there
Marc Kupietz669c0432025-07-12 12:33:42 +0200225 console.log(`Navigating to: ${this.korap_url}`);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200226 await page.goto(this.korap_url, { waitUntil: 'domcontentloaded' });
Marc Kupietz669c0432025-07-12 12:33:42 +0200227 console.log("Navigation completed");
228
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200229 // Click the vc-choose element to open corpus selection
Marc Kupietz669c0432025-07-12 12:33:42 +0200230 console.log("Waiting for #vc-choose selector...");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200231 await page.waitForSelector('#vc-choose', { visible: true, timeout: 90000 });
Marc Kupietz669c0432025-07-12 12:33:42 +0200232 console.log("Found #vc-choose, clicking...");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200233 await page.click('#vc-choose');
Marc Kupietz669c0432025-07-12 12:33:42 +0200234 console.log("Clicked #vc-choose");
235
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200236 // Wait a moment for the UI to respond
Marc Kupietz669c0432025-07-12 12:33:42 +0200237 console.log("Waiting 1 second for UI to respond...");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200238 await new Promise(resolve => setTimeout(resolve, 1000));
Marc Kupietz669c0432025-07-12 12:33:42 +0200239
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200240 // Click the statistic element
Marc Kupietz669c0432025-07-12 12:33:42 +0200241 console.log("Waiting for .statistic selector...");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200242 await page.waitForSelector('.statistic', { visible: true, timeout: 90000 });
Marc Kupietz669c0432025-07-12 12:33:42 +0200243 console.log("Found .statistic element, attempting to click...");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200244 try {
245 await page.click('.statistic');
Marc Kupietz669c0432025-07-12 12:33:42 +0200246 console.log("Successfully clicked .statistic element");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200247 } catch (error) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200248 console.error(`Failed to click statistic element: ${error.message}`);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200249 throw new Error(`Failed to click statistic element: ${error.message}`);
250 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200251
252 // Wait for statistics to load with a more efficient approach
253 console.log("Waiting for token statistics to load...");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200254
Marc Kupietz669c0432025-07-12 12:33:42 +0200255 // First, wait for any dd elements to appear (basic structure)
256 await page.waitForSelector('dd', { visible: true, timeout: 30000 });
257
258 // Then wait for the specific token statistics with a simplified check
Marc Kupietz93d7f702025-06-27 15:41:48 +0200259 await page.waitForFunction(() => {
Marc Kupietz669c0432025-07-12 12:33:42 +0200260 // Simplified check - look for any dd element with a large number
Marc Kupietz93d7f702025-06-27 15:41:48 +0200261 const ddElements = document.querySelectorAll('dd');
Marc Kupietz669c0432025-07-12 12:33:42 +0200262 for (let i = 0; i < ddElements.length; i++) {
263 const text = ddElements[i].textContent || ddElements[i].innerText || '';
Marc Kupietz93d7f702025-06-27 15:41:48 +0200264 const cleanedText = text.replace(/[,\.]/g, '');
265 const numbers = cleanedText.match(/\d+/g);
266 if (numbers && numbers.length > 0) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200267 const num = parseInt(numbers[0], 10);
268 if (num > 1000) { // Found a substantial number, likely loaded
269 return true;
270 }
Marc Kupietz93d7f702025-06-27 15:41:48 +0200271 }
272 }
273 return false;
Marc Kupietz669c0432025-07-12 12:33:42 +0200274 }, { timeout: 90000, polling: 1000 }); // Poll every second instead of continuously
Marc Kupietz93d7f702025-06-27 15:41:48 +0200275
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200276 // Look for the tokens count in a dd element that follows an element with title "tokens"
Marc Kupietz669c0432025-07-12 12:33:42 +0200277 console.log(`Starting token count extraction with minThreshold: ${minTokenThreshold}`);
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200278 const tokenCount = await page.evaluate((minThreshold) => {
Marc Kupietz669c0432025-07-12 12:33:42 +0200279 // Find the element with title "tokens" first
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200280 const tokenTitleElements = document.querySelectorAll('[title="tokens"], [title*="token"]');
281
Marc Kupietz669c0432025-07-12 12:33:42 +0200282 for (let i = 0; i < tokenTitleElements.length; i++) {
283 const element = tokenTitleElements[i];
284
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200285 // Look for the next dd element
286 let nextElement = element.nextElementSibling;
Marc Kupietz669c0432025-07-12 12:33:42 +0200287 let siblingCount = 0;
288 while (nextElement && siblingCount < 10) {
289 siblingCount++;
290
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200291 if (nextElement.tagName.toLowerCase() === 'dd') {
292 const text = nextElement.textContent || nextElement.innerText || '';
293 // Remove number separators (commas and periods) and extract number
294 const cleanedText = text.replace(/[,\.]/g, '');
295 const numbers = cleanedText.match(/\d+/g);
296 if (numbers && numbers.length > 0) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200297 const tokenValue = parseInt(numbers[0], 10);
298 return tokenValue;
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200299 }
300 }
301 nextElement = nextElement.nextElementSibling;
302 }
303 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200304
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200305 // Alternative approach: look for dd elements that contain large numbers
306 const ddElements = document.querySelectorAll('dd');
Marc Kupietz669c0432025-07-12 12:33:42 +0200307 const candidateTokenCounts = [];
308
309 for (let i = 0; i < ddElements.length; i++) {
310 const dd = ddElements[i];
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200311 const text = dd.textContent || dd.innerText || '';
312 // Remove separators and check if it's a large number (likely token count)
313 const cleanedText = text.replace(/[,\.]/g, '');
314 const numbers = cleanedText.match(/\d+/g);
315 if (numbers && numbers.length > 0) {
316 const num = parseInt(numbers[0], 10);
Marc Kupietz669c0432025-07-12 12:33:42 +0200317
318 // Use the provided threshold to filter candidates
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200319 if (num > minThreshold) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200320 candidateTokenCounts.push({ value: num, text: text, index: i });
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200321 }
322 }
323 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200324
325 if (candidateTokenCounts.length > 0) {
326 // Return the largest candidate (most likely to be the total token count)
327 const bestCandidate = candidateTokenCounts.reduce((max, current) =>
328 current.value > max.value ? current : max
329 );
330 return bestCandidate.value;
331 }
332
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200333 return null;
334 }, minTokenThreshold);
Marc Kupietz669c0432025-07-12 12:33:42 +0200335
336 console.log(`Token count extraction completed. Result: ${tokenCount}`);
337
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200338 if (tokenCount === null) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200339 console.error("ERROR: Token count extraction returned null");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200340 throw new Error("Could not find token count in corpus statistics");
341 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200342
343 console.log(`SUCCESS: Found token count: ${tokenCount}, threshold was: ${minTokenThreshold}`);
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200344 return tokenCount;
Marc Kupietz669c0432025-07-12 12:33:42 +0200345
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200346 } catch (error) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200347 console.error(`ERROR in check_corpus_statistics: ${error.message}`);
348 console.error("Full error stack:", error.stack);
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200349 throw new Error(`Failed to check corpus statistics: ${error.message}`);
350 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100351 }
352}
353
Marc Kupietz93d7f702025-06-27 15:41:48 +0200354module.exports = KorAPRC