blob: a0b0a791d8f8927b2e0864d3f20b4261d62c9a8a [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 Kupietz819a52d2026-03-21 09:41:28 +010026const KORAP_HEADLESS = !(process.env.KORAP_HEADLESS === 'false' || process.env.KORAP_HEADLESS === '0');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010027const korap_rc = require('../lib/korap_rc.js').new(KORAP_URL)
Marc Kupietz2f17a762025-12-06 11:45:49 +010028const { sendToNextcloudTalk, ifConditionIt } = require('../lib/utils.js');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010029
Marc Kupietz7f1666a2024-07-12 18:35:31 +020030const slack_webhook = process.env.SLACK_WEBHOOK_URL;
Marc Kupietz4d335a32024-09-04 16:13:48 +020031
Marc Kupietz7f1666a2024-07-12 18:35:31 +020032if (slack_webhook) {
33 slack = require('slack-notify')(slack_webhook);
34}
35
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010036
Marc Kupietzc4077822022-12-03 15:32:40 +010037
Marc Kupietz0f6c54d2022-12-03 15:32:40 +010038describe('Running KorAP UI end-to-end tests on ' + KORAP_URL, () => {
Marc Kupietzc4077822022-12-03 15:32:40 +010039
Marc Kupietz93d7f702025-06-27 15:41:48 +020040 let browser;
41 let page;
42
43
Marc Kupietzc4077822022-12-03 15:32:40 +010044 before(async () => {
Marc Kupietzb4d62a82026-06-04 14:52:28 +020045 try {
46 browser = await puppeteer.launch({
47 headless: KORAP_HEADLESS ? "shell" : false,
48 args: [
49 '--no-sandbox',
50 '--disable-setuid-sandbox',
51 '--disable-dev-shm-usage',
52 '--disable-accelerated-2d-canvas',
53 '--no-first-run',
54 '--no-zygote',
55 '--disable-gpu'
56 ]
57 })
58 page = await browser.newPage()
59 await page.setViewport({
60 width: 1980,
61 height: 768,
62 deviceScaleFactor: 1,
63 });
Marc Kupietz56981542026-06-16 09:09:44 +020064 console.log(`Run started ${new Date().toISOString()} on ${KORAP_URL}` +
65 (KORAP_VC ? ` (virtual corpus: ${KORAP_VC})` : ''));
Marc Kupietzb4d62a82026-06-04 14:52:28 +020066 } catch (error) {
67 console.error('Failed to initialize Puppeteer browser:', error.message);
68
69 // Send failure notification
70 const emoji = '🚨';
71 const message = `${emoji} Test setup on ${KORAP_URL} failed: **${error.message}**`;
72
73 // Send notification to Slack
74 if (slack) {
75 try {
76 slack.alert({
77 text: `${emoji} Test setup on ${KORAP_URL} failed`,
78 attachments: [{
79 color: 'danger',
80 fields: [{
81 title: 'Error Details',
82 value: error.message,
83 short: false
84 }, {
85 title: 'URL',
86 value: KORAP_URL,
87 short: true
88 }]
89 }]
90 });
91 } catch (slackError) {
92 console.error('Failed to send setup error to Slack:', slackError.message);
93 }
94 }
95
96 // Send notification to Nextcloud Talk
97 try {
98 await sendToNextcloudTalk(message, false);
99 } catch (ncError) {
100 console.error('Failed to send setup error to Nextcloud Talk:', ncError.message);
101 }
102
103 // Rethrow the error so that the test run is registered as failed
104 throw error;
105 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100106 })
107
Marc Kupietz93d7f702025-06-27 15:41:48 +0200108 after(async function() {
109 if (browser && typeof browser.close === 'function') {
110 await browser.close();
111 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100112 })
113
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100114 afterEach(async function () {
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100115 const testPassed = this.currentTest.state === "passed";
116 const testFailed = this.currentTest.state === "failed";
117
118 // Determine if we should send notification based on NOTIFY_ON_SUCCESS setting
119 const shouldNotify = NOTIFY_ON_SUCCESS ? testPassed : testFailed;
120
121 if (shouldNotify) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100122 // Only take screenshot if it's not one of the initial connectivity/SSL tests
123 const initialTestTitles = [
124 'should be reachable',
125 'should have a valid SSL certificate'
126 ];
127 let screenshotPath = null;
128
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100129 // Only take screenshots for failures (not for success notifications)
130 if (testFailed && !initialTestTitles.includes(this.currentTest.title) && page) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100131 screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
132 await page.screenshot({ path: screenshotPath });
133 }
134
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100135 // Prepare notification content based on success/failure
136 const emoji = testPassed ? '✅' : '🚨';
137 const status = testPassed ? 'passed' : 'failed';
138 const color = testPassed ? 'good' : 'danger';
Marc Kupietz56981542026-06-16 09:09:44 +0200139 const timestamp = new Date().toISOString();
140
141 // Capture the actual page URL (e.g. the full search results URL with
142 // query and cq) so issues can be reproduced/traced immediately. Fall
143 // back to the instance URL for tests that never navigated (about:blank).
144 let currentUrl = KORAP_URL;
145 try {
146 const u = page && typeof page.url === 'function' ? page.url() : '';
147 if (/^https?:/.test(u)) currentUrl = u;
148 } catch (e) { /* keep KORAP_URL */ }
149
150 // Echo the result with its URL to the console log as well.
151 console.log(`${emoji} [${timestamp}] ${status}: ${this.currentTest.title} — ${currentUrl}`);
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100152
Marc Kupietz65dea512025-12-04 17:55:57 +0100153 // Send notification to Slack
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200154 if (slack) {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200155 try {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200156 slack.alert({
Marc Kupietz56981542026-06-16 09:09:44 +0200157 text: `${emoji} [${timestamp}] Test on ${KORAP_URL} ${status}: *${this.currentTest.title}*`,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200158 attachments: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100159 color: color,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200160 fields: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100161 title: testPassed ? 'Passed Test' : 'Failed Test',
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200162 value: this.currentTest.title,
163 short: false
164 }, {
165 title: 'URL',
Marc Kupietz56981542026-06-16 09:09:44 +0200166 value: currentUrl,
167 short: false
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200168 }]
169 }]
170 });
171 } catch (slackError) {
172 console.error('Failed to send notification to Slack:', slackError.message);
173 }
174 }
175
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100176 // Upload screenshot to Slack if available (only for failures)
Marc Kupietz65dea512025-12-04 17:55:57 +0100177 if (screenshotPath) {
Marc Kupietz93d7f702025-06-27 15:41:48 +0200178 const slackToken = process.env.SLACK_TOKEN;
179 if (slackToken) {
180 try {
181 const { WebClient } = require('@slack/web-api');
Marc Kupietz65dea512025-12-04 17:55:57 +0100182 const fs = require('fs');
183 const web = new WebClient(slackToken);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200184 const channelId = process.env.SLACK_CHANNEL_ID || 'C07CM4JS48H';
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200185
Marc Kupietz93d7f702025-06-27 15:41:48 +0200186 const result = await web.files.uploadV2({
187 channel_id: channelId,
188 file: fs.createReadStream(screenshotPath),
189 filename: screenshotPath,
190 title: `Screenshot: ${this.currentTest.title}`,
191 initial_comment: `📸 Screenshot of failed test: ${this.currentTest.title} on ${KORAP_URL}`
192 });
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200193
Marc Kupietz93d7f702025-06-27 15:41:48 +0200194 } catch (uploadError) {
195 console.error('Failed to upload screenshot to Slack:', uploadError.message);
196 }
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200197 }
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200198 }
Marc Kupietz65dea512025-12-04 17:55:57 +0100199
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100200 // Send notification to Nextcloud Talk with screenshot (if available)
Marc Kupietz2f17a762025-12-06 11:45:49 +0100201 try {
Marc Kupietz56981542026-06-16 09:09:44 +0200202 const message = `${emoji} [${timestamp}] Test on ${KORAP_URL} ${status}: **${this.currentTest.title}**\n${currentUrl}`;
Marc Kupietz2f17a762025-12-06 11:45:49 +0100203 await sendToNextcloudTalk(message, false, screenshotPath);
204 } catch (ncError) {
205 console.error('Failed to send notification to Nextcloud Talk:', ncError.message);
Marc Kupietz65dea512025-12-04 17:55:57 +0100206 }
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100207 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200208 })
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100209
Marc Kupietz93d7f702025-06-27 15:41:48 +0200210 it('should be reachable', function (done) {
211 let doneCalled = false;
212 const url = new URL(KORAP_URL);
213 const httpModule = url.protocol === 'https:' ? https : require('http');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100214
Marc Kupietz93d7f702025-06-27 15:41:48 +0200215 const req = httpModule.request({
216 method: 'HEAD',
217 hostname: url.hostname,
218 port: url.port || (url.protocol === 'https:' ? 443 : 80),
219 path: url.pathname,
220 timeout: 5000
221 }, res => {
222 if (!doneCalled) {
223 doneCalled = true;
224 if (res.statusCode >= 200 && res.statusCode < 400) {
225 done();
226 } else {
227 done(new Error(`Server is not reachable. Status code: ${res.statusCode}`));
228 }
229 }
230 });
231 req.on('timeout', () => {
232 if (!doneCalled) {
233 doneCalled = true;
234 req.destroy();
235 done(new Error('Request to server timed out.'));
236 }
237 });
238 req.on('error', err => {
239 if (!doneCalled) {
240 doneCalled = true;
241 done(err);
242 }
243 });
244 req.end();
245 });
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100246
Marc Kupietz93d7f702025-06-27 15:41:48 +0200247 it('should have a valid SSL certificate', function (done) {
248 let doneCalled = false;
249 const url = new URL(KORAP_URL);
250 if (url.protocol !== 'https:') {
251 return this.skip();
252 }
253 const req = https.request({
254 method: 'HEAD',
255 hostname: url.hostname,
256 port: url.port || 443,
257 path: url.pathname,
258 timeout: 5000
259 }, res => {
260 if (!doneCalled) {
261 doneCalled = true;
262 const cert = res.socket.getPeerCertificate();
263 if (cert && cert.valid_to) {
264 const validTo = new Date(cert.valid_to);
265 if (validTo > new Date()) {
266 done();
267 } else {
268 done(new Error(`SSL certificate expired on ${validTo.toDateString()}`));
269 }
270 } else if (res.socket.isSessionReused()){
271 done();
272 }
273 else {
274 done(new Error('Could not retrieve SSL certificate information.'));
275 }
276 }
277 });
278 req.on('timeout', () => {
279 if (!doneCalled) {
280 doneCalled = true;
281 req.destroy();
282 done(new Error('Request to server timed out.'));
283 }
284 });
285 req.on('error', err => {
286 if (!doneCalled) {
287 doneCalled = true;
288 if (err.code === 'CERT_HAS_EXPIRED') {
289 done(new Error('SSL certificate has expired.'));
290 } else {
291 done(err);
292 }
293 }
294 });
295 req.end();
296 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100297
Marc Kupietz93d7f702025-06-27 15:41:48 +0200298 describe('UI Tests', function() {
Marc Kupietzc4077822022-12-03 15:32:40 +0100299
Marc Kupietz93d7f702025-06-27 15:41:48 +0200300 before(function() {
301 // Check the state of the parent suite's tests
302 const initialTests = this.test.parent.parent.tests;
303 if (initialTests[0].state === 'failed' || initialTests[1].state === 'failed') {
304 this.skip();
305 }
306 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100307
Marc Kupietz93d7f702025-06-27 15:41:48 +0200308
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200309
Marc Kupietz93d7f702025-06-27 15:41:48 +0200310 it('KorAP UI is up and running', async function () {
311 try {
312 await page.goto(KORAP_URL, { waitUntil: 'domcontentloaded' });
313 await page.waitForSelector("#q-field", { visible: true });
314 const query_field = await page.$("#q-field")
315 assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
316 } catch (error) {
317 throw new Error(`Failed to load KorAP UI or find query field: ${error.message}`);
318 }
Marc Kupietz0f6c54d2022-12-03 15:32:40 +0100319 })
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100320
Marc Kupietzc4077822022-12-03 15:32:40 +0100321
Marc Kupietz93d7f702025-06-27 15:41:48 +0200322 ifConditionIt('Login into KorAP with incorrect credentials fails',
323 KORAP_LOGIN != "",
324 (async () => {
325 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD + "*")
326 login_result.should.be.false
327 }))
328
329 ifConditionIt('Login into KorAP with correct credentials succeeds',
330 KORAP_LOGIN != "",
331 (async () => {
332 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
333 login_result.should.be.true
334 }))
335
336 it('Can turn glimpse off',
337 (async () => {
338 await korap_rc.assure_glimpse_off(page)
339 }))
340
341 it('Corpus statistics show sufficient tokens',
342 (async () => {
343 const tokenCount = await korap_rc.check_corpus_statistics(page, KORAP_MIN_TOKENS_IN_CORPUS);
344 console.log(`Found ${tokenCount} tokens in corpus, minimum required: ${KORAP_MIN_TOKENS_IN_CORPUS}`);
345 tokenCount.should.be.above(KORAP_MIN_TOKENS_IN_CORPUS - 1,
346 `Corpus should have at least ${KORAP_MIN_TOKENS_IN_CORPUS} tokens, but found ${tokenCount}`);
347 })).timeout(90000)
348
349 describe('Running searches that should have hits', () => {
350
Marc Kupietz6329cac2026-06-15 16:39:55 +0200351 // The searches must run while logged in — most corpora are only
352 // available after authentication. Re-assert the session here and,
353 // when a login is required, fail loudly if it didn't take instead
354 // of silently testing the (tiny) public corpus logged out.
355 before(async () => {
356 const logged_in = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
357 if (KORAP_LOGIN != "") {
358 assert.isTrue(logged_in,
359 "Login is required for the has-hits searches but did not succeed; aborting so we don't test the public corpus while logged out")
360 }
361 })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200362
Marc Kupietz56981542026-06-16 09:09:44 +0200363 const vcLabel = KORAP_VC ? ` in vc "${KORAP_VC}"` : '';
Marc Kupietz93d7f702025-06-27 15:41:48 +0200364 KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
Marc Kupietz56981542026-06-16 09:09:44 +0200365 it('Search for "' + query + '"' + vcLabel + ' has hits',
Marc Kupietz93d7f702025-06-27 15:41:48 +0200366 (async () => {
367 await korap_rc.assure_glimpse_off(page)
Marc Kupietz6329cac2026-06-15 16:39:55 +0200368 const hits = await korap_rc.search(page, query, { timeout: KORAP_SEARCH_TIMEOUT, vc: KORAP_VC })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200369 hits.should.be.above(0)
Marc Kupietz6329cac2026-06-15 16:39:55 +0200370 })).timeout(KORAP_SEARCH_TIMEOUT + 30000)
Marc Kupietz93d7f702025-06-27 15:41:48 +0200371 })
372 })
373
Marc Kupietz6329cac2026-06-15 16:39:55 +0200374 // Logout must be the LAST UI test. Mocha runs a suite's direct it()
375 // tests before its nested describe() suites, so a top-level "Logout"
376 // test would otherwise run *before* the searches above and log us out,
377 // leaving them to query the public corpus. Wrapping it in its own suite
378 // declared after the searches suite guarantees it runs last.
379 describe('Logging out', () => {
380 ifConditionIt('Logout works',
381 KORAP_LOGIN != "",
382 (async () => {
383 const logout_result = await korap_rc.logout(page)
384 logout_result.should.be.true
385 })).timeout(15000)
386 })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200387 });
388});