blob: b7341112172eee38eb4ed754117d747906684594 [file] [log] [blame]
Marc Kupietz93d7f702025-06-27 15:41:48 +02001const https = require('https');
Marc Kupietz2f17a762025-12-06 11:45:49 +01002
Marc Kupietz490b0532024-09-05 09:36:21 +02003const puppeteer = require('puppeteer-extra');
4puppeteer.use(require('puppeteer-extra-plugin-user-preferences')({
5 userPrefs: {
6 safebrowsing: {
7 enabled: false,
8 enhanced: false
9 }
10 }
11}));
Marc Kupietz55fc3162022-12-04 16:25:49 +010012const chai = require('chai');
Marc Kupietz4c5a7a52022-12-04 16:56:30 +010013const { afterEach } = require('mocha');
Marc Kupietz55fc3162022-12-04 16:25:49 +010014const assert = chai.assert;
15const should = chai.should();
Marc Kupietz7f1666a2024-07-12 18:35:31 +020016var slack = null;
Marc Kupietz55fc3162022-12-04 16:25:49 +010017
Marc Kupietz0f6c54d2022-12-03 15:32:40 +010018const KORAP_URL = process.env.KORAP_URL || "http://localhost:64543";
Marc Kupietzbfb23012025-06-03 15:47:10 +020019const KORAP_LOGIN = 'KORAP_USERNAME' in process.env ? process.env.KORAP_USERNAME : 'KORAP_LOGIN' in process.env ? process.env.KORAP_LOGIN : "user2"
20const KORAP_PWD = process.env.KORAP_PWD || process.env.KORAP_PASSWORD || "password2";
Marc Kupietz26982382022-12-04 19:02:57 +010021const KORAP_QUERIES = process.env.KORAP_QUERIES || 'geht, [orth=geht & cmc/pos=VVFIN]'
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +020022const KORAP_MIN_TOKENS_IN_CORPUS = parseInt(process.env.KORAP_MIN_TOKENS_IN_CORPUS || "100000", 10);
Marc Kupietzf0079692026-06-14 20:25:54 +020023const KORAP_SEARCH_TIMEOUT = parseInt(process.env.KORAP_SEARCH_TIMEOUT || "60000", 10);
Marc Kupietz6329cac2026-06-15 16:39:55 +020024const KORAP_VC = process.env.KORAP_VC || process.env.VC || "";
Marc Kupietzbbe6de02026-02-04 09:39:48 +010025const NOTIFY_ON_SUCCESS = process.env.NOTIFY_ON_SUCCESS === 'true' || process.env.NOTIFY_ON_SUCCESS === '1';
Marc Kupietzf44c1282026-07-06 16:04:03 +020026const KORAP_DISABLE_GLIMPSE = process.env.KORAP_DISABLE_GLIMPSE === 'true' || process.env.KORAP_DISABLE_GLIMPSE === '1';
Marc Kupietz819a52d2026-03-21 09:41:28 +010027const KORAP_HEADLESS = !(process.env.KORAP_HEADLESS === 'false' || process.env.KORAP_HEADLESS === '0');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010028const korap_rc = require('../lib/korap_rc.js').new(KORAP_URL)
Marc Kupietz2f17a762025-12-06 11:45:49 +010029const { sendToNextcloudTalk, ifConditionIt } = require('../lib/utils.js');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010030
Marc Kupietz7f1666a2024-07-12 18:35:31 +020031const slack_webhook = process.env.SLACK_WEBHOOK_URL;
Marc Kupietz4d335a32024-09-04 16:13:48 +020032
Marc Kupietz7f1666a2024-07-12 18:35:31 +020033if (slack_webhook) {
34 slack = require('slack-notify')(slack_webhook);
35}
36
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010037
Marc Kupietzc4077822022-12-03 15:32:40 +010038
Marc Kupietz0f6c54d2022-12-03 15:32:40 +010039describe('Running KorAP UI end-to-end tests on ' + KORAP_URL, () => {
Marc Kupietzc4077822022-12-03 15:32:40 +010040
Marc Kupietz93d7f702025-06-27 15:41:48 +020041 let browser;
42 let page;
43
44
Marc Kupietzc4077822022-12-03 15:32:40 +010045 before(async () => {
Marc Kupietzb4d62a82026-06-04 14:52:28 +020046 try {
47 browser = await puppeteer.launch({
48 headless: KORAP_HEADLESS ? "shell" : false,
49 args: [
50 '--no-sandbox',
51 '--disable-setuid-sandbox',
52 '--disable-dev-shm-usage',
53 '--disable-accelerated-2d-canvas',
54 '--no-first-run',
55 '--no-zygote',
56 '--disable-gpu'
57 ]
58 })
59 page = await browser.newPage()
60 await page.setViewport({
61 width: 1980,
62 height: 768,
63 deviceScaleFactor: 1,
64 });
Marc Kupietz56981542026-06-16 09:09:44 +020065 console.log(`Run started ${new Date().toISOString()} on ${KORAP_URL}` +
66 (KORAP_VC ? ` (virtual corpus: ${KORAP_VC})` : ''));
Marc Kupietzb4d62a82026-06-04 14:52:28 +020067 } catch (error) {
68 console.error('Failed to initialize Puppeteer browser:', error.message);
69
70 // Send failure notification
71 const emoji = '🚨';
72 const message = `${emoji} Test setup on ${KORAP_URL} failed: **${error.message}**`;
73
74 // Send notification to Slack
75 if (slack) {
76 try {
77 slack.alert({
78 text: `${emoji} Test setup on ${KORAP_URL} failed`,
79 attachments: [{
80 color: 'danger',
81 fields: [{
82 title: 'Error Details',
83 value: error.message,
84 short: false
85 }, {
86 title: 'URL',
87 value: KORAP_URL,
88 short: true
89 }]
90 }]
91 });
92 } catch (slackError) {
93 console.error('Failed to send setup error to Slack:', slackError.message);
94 }
95 }
96
97 // Send notification to Nextcloud Talk
98 try {
99 await sendToNextcloudTalk(message, false);
100 } catch (ncError) {
101 console.error('Failed to send setup error to Nextcloud Talk:', ncError.message);
102 }
103
104 // Rethrow the error so that the test run is registered as failed
105 throw error;
106 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100107 })
108
Marc Kupietz93d7f702025-06-27 15:41:48 +0200109 after(async function() {
110 if (browser && typeof browser.close === 'function') {
111 await browser.close();
112 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100113 })
114
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100115 afterEach(async function () {
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100116 const testPassed = this.currentTest.state === "passed";
117 const testFailed = this.currentTest.state === "failed";
118
119 // Determine if we should send notification based on NOTIFY_ON_SUCCESS setting
120 const shouldNotify = NOTIFY_ON_SUCCESS ? testPassed : testFailed;
121
122 if (shouldNotify) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100123 // Only take screenshot if it's not one of the initial connectivity/SSL tests
124 const initialTestTitles = [
125 'should be reachable',
126 'should have a valid SSL certificate'
127 ];
128 let screenshotPath = null;
129
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100130 // Only take screenshots for failures (not for success notifications)
131 if (testFailed && !initialTestTitles.includes(this.currentTest.title) && page) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100132 screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
133 await page.screenshot({ path: screenshotPath });
134 }
135
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100136 // Prepare notification content based on success/failure
137 const emoji = testPassed ? '✅' : '🚨';
138 const status = testPassed ? 'passed' : 'failed';
139 const color = testPassed ? 'good' : 'danger';
Marc Kupietz56981542026-06-16 09:09:44 +0200140 const timestamp = new Date().toISOString();
141
142 // Capture the actual page URL (e.g. the full search results URL with
143 // query and cq) so issues can be reproduced/traced immediately. Fall
144 // back to the instance URL for tests that never navigated (about:blank).
145 let currentUrl = KORAP_URL;
146 try {
147 const u = page && typeof page.url === 'function' ? page.url() : '';
148 if (/^https?:/.test(u)) currentUrl = u;
149 } catch (e) { /* keep KORAP_URL */ }
150
151 // Echo the result with its URL to the console log as well.
152 console.log(`${emoji} [${timestamp}] ${status}: ${this.currentTest.title} — ${currentUrl}`);
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100153
Marc Kupietz65dea512025-12-04 17:55:57 +0100154 // Send notification to Slack
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200155 if (slack) {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200156 try {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200157 slack.alert({
Marc Kupietz56981542026-06-16 09:09:44 +0200158 text: `${emoji} [${timestamp}] Test on ${KORAP_URL} ${status}: *${this.currentTest.title}*`,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200159 attachments: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100160 color: color,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200161 fields: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100162 title: testPassed ? 'Passed Test' : 'Failed Test',
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200163 value: this.currentTest.title,
164 short: false
165 }, {
166 title: 'URL',
Marc Kupietz56981542026-06-16 09:09:44 +0200167 value: currentUrl,
168 short: false
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200169 }]
170 }]
171 });
172 } catch (slackError) {
173 console.error('Failed to send notification to Slack:', slackError.message);
174 }
175 }
176
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100177 // Upload screenshot to Slack if available (only for failures)
Marc Kupietz65dea512025-12-04 17:55:57 +0100178 if (screenshotPath) {
Marc Kupietz93d7f702025-06-27 15:41:48 +0200179 const slackToken = process.env.SLACK_TOKEN;
180 if (slackToken) {
181 try {
182 const { WebClient } = require('@slack/web-api');
Marc Kupietz65dea512025-12-04 17:55:57 +0100183 const fs = require('fs');
184 const web = new WebClient(slackToken);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200185 const channelId = process.env.SLACK_CHANNEL_ID || 'C07CM4JS48H';
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200186
Marc Kupietz93d7f702025-06-27 15:41:48 +0200187 const result = await web.files.uploadV2({
188 channel_id: channelId,
189 file: fs.createReadStream(screenshotPath),
190 filename: screenshotPath,
191 title: `Screenshot: ${this.currentTest.title}`,
192 initial_comment: `📸 Screenshot of failed test: ${this.currentTest.title} on ${KORAP_URL}`
193 });
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200194
Marc Kupietz93d7f702025-06-27 15:41:48 +0200195 } catch (uploadError) {
196 console.error('Failed to upload screenshot to Slack:', uploadError.message);
197 }
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200198 }
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200199 }
Marc Kupietz65dea512025-12-04 17:55:57 +0100200
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100201 // Send notification to Nextcloud Talk with screenshot (if available)
Marc Kupietz2f17a762025-12-06 11:45:49 +0100202 try {
Marc Kupietz56981542026-06-16 09:09:44 +0200203 const message = `${emoji} [${timestamp}] Test on ${KORAP_URL} ${status}: **${this.currentTest.title}**\n${currentUrl}`;
Marc Kupietz2f17a762025-12-06 11:45:49 +0100204 await sendToNextcloudTalk(message, false, screenshotPath);
205 } catch (ncError) {
206 console.error('Failed to send notification to Nextcloud Talk:', ncError.message);
Marc Kupietz65dea512025-12-04 17:55:57 +0100207 }
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100208 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200209 })
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100210
Marc Kupietz93d7f702025-06-27 15:41:48 +0200211 it('should be reachable', function (done) {
212 let doneCalled = false;
213 const url = new URL(KORAP_URL);
214 const httpModule = url.protocol === 'https:' ? https : require('http');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100215
Marc Kupietz93d7f702025-06-27 15:41:48 +0200216 const req = httpModule.request({
217 method: 'HEAD',
218 hostname: url.hostname,
219 port: url.port || (url.protocol === 'https:' ? 443 : 80),
220 path: url.pathname,
221 timeout: 5000
222 }, res => {
223 if (!doneCalled) {
224 doneCalled = true;
225 if (res.statusCode >= 200 && res.statusCode < 400) {
226 done();
227 } else {
228 done(new Error(`Server is not reachable. Status code: ${res.statusCode}`));
229 }
230 }
231 });
232 req.on('timeout', () => {
233 if (!doneCalled) {
234 doneCalled = true;
235 req.destroy();
236 done(new Error('Request to server timed out.'));
237 }
238 });
239 req.on('error', err => {
240 if (!doneCalled) {
241 doneCalled = true;
242 done(err);
243 }
244 });
245 req.end();
246 });
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100247
Marc Kupietz93d7f702025-06-27 15:41:48 +0200248 it('should have a valid SSL certificate', function (done) {
249 let doneCalled = false;
250 const url = new URL(KORAP_URL);
251 if (url.protocol !== 'https:') {
252 return this.skip();
253 }
254 const req = https.request({
255 method: 'HEAD',
256 hostname: url.hostname,
257 port: url.port || 443,
258 path: url.pathname,
259 timeout: 5000
260 }, res => {
261 if (!doneCalled) {
262 doneCalled = true;
263 const cert = res.socket.getPeerCertificate();
264 if (cert && cert.valid_to) {
265 const validTo = new Date(cert.valid_to);
266 if (validTo > new Date()) {
267 done();
268 } else {
269 done(new Error(`SSL certificate expired on ${validTo.toDateString()}`));
270 }
271 } else if (res.socket.isSessionReused()){
272 done();
273 }
274 else {
275 done(new Error('Could not retrieve SSL certificate information.'));
276 }
277 }
278 });
279 req.on('timeout', () => {
280 if (!doneCalled) {
281 doneCalled = true;
282 req.destroy();
283 done(new Error('Request to server timed out.'));
284 }
285 });
286 req.on('error', err => {
287 if (!doneCalled) {
288 doneCalled = true;
289 if (err.code === 'CERT_HAS_EXPIRED') {
290 done(new Error('SSL certificate has expired.'));
291 } else {
292 done(err);
293 }
294 }
295 });
296 req.end();
297 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100298
Marc Kupietz93d7f702025-06-27 15:41:48 +0200299 describe('UI Tests', function() {
Marc Kupietzc4077822022-12-03 15:32:40 +0100300
Marc Kupietz93d7f702025-06-27 15:41:48 +0200301 before(function() {
302 // Check the state of the parent suite's tests
303 const initialTests = this.test.parent.parent.tests;
304 if (initialTests[0].state === 'failed' || initialTests[1].state === 'failed') {
305 this.skip();
306 }
307 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100308
Marc Kupietz93d7f702025-06-27 15:41:48 +0200309
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200310
Marc Kupietz93d7f702025-06-27 15:41:48 +0200311 it('KorAP UI is up and running', async function () {
312 try {
313 await page.goto(KORAP_URL, { waitUntil: 'domcontentloaded' });
314 await page.waitForSelector("#q-field", { visible: true });
315 const query_field = await page.$("#q-field")
316 assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
317 } catch (error) {
318 throw new Error(`Failed to load KorAP UI or find query field: ${error.message}`);
319 }
Marc Kupietz0f6c54d2022-12-03 15:32:40 +0100320 })
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100321
Marc Kupietzc4077822022-12-03 15:32:40 +0100322
Marc Kupietz93d7f702025-06-27 15:41:48 +0200323 ifConditionIt('Login into KorAP with incorrect credentials fails',
324 KORAP_LOGIN != "",
325 (async () => {
326 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD + "*")
327 login_result.should.be.false
328 }))
329
330 ifConditionIt('Login into KorAP with correct credentials succeeds',
331 KORAP_LOGIN != "",
332 (async () => {
333 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
334 login_result.should.be.true
335 }))
336
Marc Kupietz93d7f702025-06-27 15:41:48 +0200337 it('Corpus statistics show sufficient tokens',
338 (async () => {
339 const tokenCount = await korap_rc.check_corpus_statistics(page, KORAP_MIN_TOKENS_IN_CORPUS);
340 console.log(`Found ${tokenCount} tokens in corpus, minimum required: ${KORAP_MIN_TOKENS_IN_CORPUS}`);
341 tokenCount.should.be.above(KORAP_MIN_TOKENS_IN_CORPUS - 1,
342 `Corpus should have at least ${KORAP_MIN_TOKENS_IN_CORPUS} tokens, but found ${tokenCount}`);
343 })).timeout(90000)
344
345 describe('Running searches that should have hits', () => {
346
Marc Kupietz6329cac2026-06-15 16:39:55 +0200347 // The searches must run while logged in — most corpora are only
348 // available after authentication. Re-assert the session here and,
349 // when a login is required, fail loudly if it didn't take instead
350 // of silently testing the (tiny) public corpus logged out.
351 before(async () => {
352 const logged_in = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
353 if (KORAP_LOGIN != "") {
354 assert.isTrue(logged_in,
355 "Login is required for the has-hits searches but did not succeed; aborting so we don't test the public corpus while logged out")
356 }
357 })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200358
Marc Kupietz56981542026-06-16 09:09:44 +0200359 const vcLabel = KORAP_VC ? ` in vc "${KORAP_VC}"` : '';
Marc Kupietz93d7f702025-06-27 15:41:48 +0200360 KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
Marc Kupietz56981542026-06-16 09:09:44 +0200361 it('Search for "' + query + '"' + vcLabel + ' has hits',
Marc Kupietz93d7f702025-06-27 15:41:48 +0200362 (async () => {
Marc Kupietzf44c1282026-07-06 16:04:03 +0200363 // Leave glimpse (cutoff) on by default to reduce server load; it
364 // still proves the server is up. Set KORAP_DISABLE_GLIMPSE=true
365 // to force full (uncapped) result counts if needed.
366 if (KORAP_DISABLE_GLIMPSE) {
367 await korap_rc.assure_glimpse_off(page)
368 }
Marc Kupietz6329cac2026-06-15 16:39:55 +0200369 const hits = await korap_rc.search(page, query, { timeout: KORAP_SEARCH_TIMEOUT, vc: KORAP_VC })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200370 hits.should.be.above(0)
Marc Kupietz6329cac2026-06-15 16:39:55 +0200371 })).timeout(KORAP_SEARCH_TIMEOUT + 30000)
Marc Kupietz93d7f702025-06-27 15:41:48 +0200372 })
373 })
374
Marc Kupietz6329cac2026-06-15 16:39:55 +0200375 // Logout must be the LAST UI test. Mocha runs a suite's direct it()
376 // tests before its nested describe() suites, so a top-level "Logout"
377 // test would otherwise run *before* the searches above and log us out,
378 // leaving them to query the public corpus. Wrapping it in its own suite
379 // declared after the searches suite guarantees it runs last.
380 describe('Logging out', () => {
381 ifConditionIt('Logout works',
382 KORAP_LOGIN != "",
383 (async () => {
384 const logout_result = await korap_rc.logout(page)
385 logout_result.should.be.true
386 })).timeout(15000)
387 })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200388 });
389});