blob: 0bdf1af1e6a451146eb1ffd126350859f0bc8636 [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
73 async search(page, query) {
Marc Kupietz964e7772025-06-03 15:02:30 +020074 try {
Marc Kupietz93d7f702025-06-27 15:41:48 +020075 await page.waitForSelector("#q-field", { visible: true });
76 const query_field = await page.$("#q-field");
77 assert.notEqual(query_field, null, "Query field not found");
Marc Kupietz964e7772025-06-03 15:02:30 +020078
Marc Kupietz93d7f702025-06-27 15:41:48 +020079 await query_field.click({ clickCount: 3 });
80 await page.keyboard.type(query);
81 await page.keyboard.press("Enter");
Marc Kupietz964e7772025-06-03 15:02:30 +020082
Marc Kupietz93d7f702025-06-27 15:41:48 +020083 await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
84
Marc Kupietzf0079692026-06-14 20:25:54 +020085 // Wait until the results page has actually settled, then read the
86 // count immediately (no fixed sleep). Kalamar renders the precise
87 // total into #total-results (e.g. "1,039,193") on hits, but when
88 // glimpse/cutoff is on it lists matches without computing a total,
89 // and on a miss it shows a .no-results message. A search "has hits"
90 // as soon as any match is listed, so settle on whichever appears.
91 await page.waitForFunction(() => {
92 const total = document.querySelector('#total-results');
93 if (total && /\d/.test(total.textContent || '')) return true;
94 if (document.querySelectorAll('#search ol li').length > 0) return true;
95 return document.querySelector('#search .no-results, .no-results') !== null;
96 }, { timeout: 15000, polling: 200 });
Marc Kupietz93d7f702025-06-27 15:41:48 +020097
Marc Kupietzf0079692026-06-14 20:25:54 +020098 const hits = await page.evaluate(() => {
99 const total = document.querySelector('#total-results');
100 if (total) {
101 // The span holds only the number; strip thousands separators
102 // (commas, periods, spaces) and parse what remains.
103 const digits = (total.textContent || '').replace(/[^\d]/g, '');
104 if (digits.length > 0) return parseInt(digits, 10);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200105 }
106
Marc Kupietzf0079692026-06-14 20:25:54 +0200107 // No exact total (e.g. glimpse/cutoff on): listed matches still
108 // prove the query has hits, so fall back to counting them.
109 const items = document.querySelectorAll('#search ol li').length;
110 if (items > 0) return items;
Marc Kupietz964e7772025-06-03 15:02:30 +0200111
Marc Kupietzf0079692026-06-14 20:25:54 +0200112 // Explicit "no matches" state reported by Kalamar.
113 return 0;
Marc Kupietz964e7772025-06-03 15:02:30 +0200114 });
115
Marc Kupietz93d7f702025-06-27 15:41:48 +0200116 return hits;
117 } catch (error) {
118 throw new Error(`Failed to perform search: ${error.message}`);
Marc Kupietz964e7772025-06-03 15:02:30 +0200119 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100120 }
121
122 async logout(page) {
Marc Kupietz964e7772025-06-03 15:02:30 +0200123 try {
Marc Kupietz96aa4152026-06-12 17:20:32 +0200124 // First, try to find the logout link in the page
125 const logoutHref = await page.evaluate(() => {
126 const logoutEl = document.querySelector('a.logout, a[data-testid="logout"]');
127 return logoutEl ? logoutEl.getAttribute('href') : null;
128 });
Marc Kupietz964e7772025-06-03 15:02:30 +0200129
Marc Kupietz96aa4152026-06-12 17:20:32 +0200130 if (logoutHref) {
131 const absoluteLogoutUrl = new URL(logoutHref, page.url()).href;
132 await page.goto(absoluteLogoutUrl, { waitUntil: 'domcontentloaded', timeout: 10000 });
133 } else {
134 const currentUrl = await page.url();
135 const baseUrl = currentUrl.replace(/\/$/, '');
136 try {
137 await page.goto(baseUrl + '/user/logout', { waitUntil: 'domcontentloaded', timeout: 5000 });
138 } catch (e) {
139 await page.goto(baseUrl + '/logout', { waitUntil: 'domcontentloaded', timeout: 5000 });
140 }
141 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200142
143 // Navigate back to main page to ensure clean state for subsequent tests
144 await page.goto(this.korap_url, { waitUntil: 'domcontentloaded', timeout: 10000 });
145
Marc Kupietz96aa4152026-06-12 17:20:32 +0200146 // Verify we are actually logged out
147 const loggedOut = await page.evaluate(() => {
148 return document.querySelector('a.logout, a[data-testid="logout"], .dropdown-btn.profile') === null;
149 });
150
151 return loggedOut;
Marc Kupietz964e7772025-06-03 15:02:30 +0200152 } catch (error) {
Marc Kupietz96aa4152026-06-12 17:20:32 +0200153 console.error(`Logout failed: ${error.message}`);
Marc Kupietz964e7772025-06-03 15:02:30 +0200154 return false;
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100155 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100156 }
157
158 async assure_glimpse_off(page) {
Marc Kupietz0462c7d2026-03-21 10:10:00 +0100159 // Get the cutoff checkbox - works in both old and new Kalamar versions
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100160 const glimpse = await page.$("input[name=cutoff]")
Marc Kupietz0462c7d2026-03-21 10:10:00 +0100161 if (!glimpse) {
162 console.log("Glimpse checkbox not found, skipping")
163 return
164 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100165 const glimpse_value = await (await glimpse.getProperty('checked')).jsonValue()
166 if (glimpse_value) {
Marc Kupietz0462c7d2026-03-21 10:10:00 +0100167 // Try new Kalamar version first (toggle button with class 'glimpse')
168 const newGlimpseButton = await page.$(".glimpse")
169 if (newGlimpseButton) {
170 const isVisible = await page.evaluate(el => {
171 const style = window.getComputedStyle(el)
172 return style.display !== 'none' && style.visibility !== 'hidden'
173 }, newGlimpseButton)
174 if (isVisible) {
175 await newGlimpseButton.click()
176 return
177 }
178 }
179 // Fall back to old Kalamar version (label with id 'glimpse')
180 const oldGlimpseLabel = await page.$("#glimpse")
181 if (oldGlimpseLabel) {
182 const isVisible = await page.evaluate(el => {
183 const style = window.getComputedStyle(el.parentNode || el)
184 return style.display !== 'none' && style.visibility !== 'hidden'
185 }, oldGlimpseLabel)
186 if (isVisible) {
187 await page.click("#glimpse")
188 return
189 }
190 }
191 // Last resort: directly toggle the checkbox via JavaScript
192 await page.evaluate(() => {
193 const checkbox = document.querySelector("input[name=cutoff]")
194 if (checkbox) checkbox.checked = false
195 })
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100196 }
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200197 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100198
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200199 async check_corpus_statistics(page, minTokenThreshold = 1000) {
200 try {
Marc Kupietz669c0432025-07-12 12:33:42 +0200201 console.log(`Starting corpus statistics check with minTokenThreshold: ${minTokenThreshold}`);
202
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200203 // Navigate to the corpus view if not already there
Marc Kupietz669c0432025-07-12 12:33:42 +0200204 console.log(`Navigating to: ${this.korap_url}`);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200205 await page.goto(this.korap_url, { waitUntil: 'domcontentloaded' });
Marc Kupietz669c0432025-07-12 12:33:42 +0200206 console.log("Navigation completed");
207
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200208 // Click the vc-choose element to open corpus selection
Marc Kupietz669c0432025-07-12 12:33:42 +0200209 console.log("Waiting for #vc-choose selector...");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200210 await page.waitForSelector('#vc-choose', { visible: true, timeout: 90000 });
Marc Kupietz669c0432025-07-12 12:33:42 +0200211 console.log("Found #vc-choose, clicking...");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200212 await page.click('#vc-choose');
Marc Kupietz669c0432025-07-12 12:33:42 +0200213 console.log("Clicked #vc-choose");
214
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200215 // Wait a moment for the UI to respond
Marc Kupietz669c0432025-07-12 12:33:42 +0200216 console.log("Waiting 1 second for UI to respond...");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200217 await new Promise(resolve => setTimeout(resolve, 1000));
Marc Kupietz669c0432025-07-12 12:33:42 +0200218
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200219 // Click the statistic element
Marc Kupietz669c0432025-07-12 12:33:42 +0200220 console.log("Waiting for .statistic selector...");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200221 await page.waitForSelector('.statistic', { visible: true, timeout: 90000 });
Marc Kupietz669c0432025-07-12 12:33:42 +0200222 console.log("Found .statistic element, attempting to click...");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200223 try {
224 await page.click('.statistic');
Marc Kupietz669c0432025-07-12 12:33:42 +0200225 console.log("Successfully clicked .statistic element");
Marc Kupietz93d7f702025-06-27 15:41:48 +0200226 } catch (error) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200227 console.error(`Failed to click statistic element: ${error.message}`);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200228 throw new Error(`Failed to click statistic element: ${error.message}`);
229 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200230
231 // Wait for statistics to load with a more efficient approach
232 console.log("Waiting for token statistics to load...");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200233
Marc Kupietz669c0432025-07-12 12:33:42 +0200234 // First, wait for any dd elements to appear (basic structure)
235 await page.waitForSelector('dd', { visible: true, timeout: 30000 });
236
237 // Then wait for the specific token statistics with a simplified check
Marc Kupietz93d7f702025-06-27 15:41:48 +0200238 await page.waitForFunction(() => {
Marc Kupietz669c0432025-07-12 12:33:42 +0200239 // Simplified check - look for any dd element with a large number
Marc Kupietz93d7f702025-06-27 15:41:48 +0200240 const ddElements = document.querySelectorAll('dd');
Marc Kupietz669c0432025-07-12 12:33:42 +0200241 for (let i = 0; i < ddElements.length; i++) {
242 const text = ddElements[i].textContent || ddElements[i].innerText || '';
Marc Kupietz93d7f702025-06-27 15:41:48 +0200243 const cleanedText = text.replace(/[,\.]/g, '');
244 const numbers = cleanedText.match(/\d+/g);
245 if (numbers && numbers.length > 0) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200246 const num = parseInt(numbers[0], 10);
247 if (num > 1000) { // Found a substantial number, likely loaded
248 return true;
249 }
Marc Kupietz93d7f702025-06-27 15:41:48 +0200250 }
251 }
252 return false;
Marc Kupietz669c0432025-07-12 12:33:42 +0200253 }, { timeout: 90000, polling: 1000 }); // Poll every second instead of continuously
Marc Kupietz93d7f702025-06-27 15:41:48 +0200254
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200255 // Look for the tokens count in a dd element that follows an element with title "tokens"
Marc Kupietz669c0432025-07-12 12:33:42 +0200256 console.log(`Starting token count extraction with minThreshold: ${minTokenThreshold}`);
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200257 const tokenCount = await page.evaluate((minThreshold) => {
Marc Kupietz669c0432025-07-12 12:33:42 +0200258 // Find the element with title "tokens" first
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200259 const tokenTitleElements = document.querySelectorAll('[title="tokens"], [title*="token"]');
260
Marc Kupietz669c0432025-07-12 12:33:42 +0200261 for (let i = 0; i < tokenTitleElements.length; i++) {
262 const element = tokenTitleElements[i];
263
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200264 // Look for the next dd element
265 let nextElement = element.nextElementSibling;
Marc Kupietz669c0432025-07-12 12:33:42 +0200266 let siblingCount = 0;
267 while (nextElement && siblingCount < 10) {
268 siblingCount++;
269
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200270 if (nextElement.tagName.toLowerCase() === 'dd') {
271 const text = nextElement.textContent || nextElement.innerText || '';
272 // Remove number separators (commas and periods) and extract number
273 const cleanedText = text.replace(/[,\.]/g, '');
274 const numbers = cleanedText.match(/\d+/g);
275 if (numbers && numbers.length > 0) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200276 const tokenValue = parseInt(numbers[0], 10);
277 return tokenValue;
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200278 }
279 }
280 nextElement = nextElement.nextElementSibling;
281 }
282 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200283
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200284 // Alternative approach: look for dd elements that contain large numbers
285 const ddElements = document.querySelectorAll('dd');
Marc Kupietz669c0432025-07-12 12:33:42 +0200286 const candidateTokenCounts = [];
287
288 for (let i = 0; i < ddElements.length; i++) {
289 const dd = ddElements[i];
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200290 const text = dd.textContent || dd.innerText || '';
291 // Remove separators and check if it's a large number (likely token count)
292 const cleanedText = text.replace(/[,\.]/g, '');
293 const numbers = cleanedText.match(/\d+/g);
294 if (numbers && numbers.length > 0) {
295 const num = parseInt(numbers[0], 10);
Marc Kupietz669c0432025-07-12 12:33:42 +0200296
297 // Use the provided threshold to filter candidates
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200298 if (num > minThreshold) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200299 candidateTokenCounts.push({ value: num, text: text, index: i });
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200300 }
301 }
302 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200303
304 if (candidateTokenCounts.length > 0) {
305 // Return the largest candidate (most likely to be the total token count)
306 const bestCandidate = candidateTokenCounts.reduce((max, current) =>
307 current.value > max.value ? current : max
308 );
309 return bestCandidate.value;
310 }
311
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200312 return null;
313 }, minTokenThreshold);
Marc Kupietz669c0432025-07-12 12:33:42 +0200314
315 console.log(`Token count extraction completed. Result: ${tokenCount}`);
316
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200317 if (tokenCount === null) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200318 console.error("ERROR: Token count extraction returned null");
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200319 throw new Error("Could not find token count in corpus statistics");
320 }
Marc Kupietz669c0432025-07-12 12:33:42 +0200321
322 console.log(`SUCCESS: Found token count: ${tokenCount}, threshold was: ${minTokenThreshold}`);
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200323 return tokenCount;
Marc Kupietz669c0432025-07-12 12:33:42 +0200324
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200325 } catch (error) {
Marc Kupietz669c0432025-07-12 12:33:42 +0200326 console.error(`ERROR in check_corpus_statistics: ${error.message}`);
327 console.error("Full error stack:", error.stack);
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200328 throw new Error(`Failed to check corpus statistics: ${error.message}`);
329 }
Marc Kupietz5e45a2f2022-12-03 15:32:40 +0100330 }
331}
332
Marc Kupietz93d7f702025-06-27 15:41:48 +0200333module.exports = KorAPRC