blob: aaf46c17b03008dec7711fc0781f49c576ecd669 [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 });
64 } catch (error) {
65 console.error('Failed to initialize Puppeteer browser:', error.message);
66
67 // Send failure notification
68 const emoji = '🚨';
69 const message = `${emoji} Test setup on ${KORAP_URL} failed: **${error.message}**`;
70
71 // Send notification to Slack
72 if (slack) {
73 try {
74 slack.alert({
75 text: `${emoji} Test setup on ${KORAP_URL} failed`,
76 attachments: [{
77 color: 'danger',
78 fields: [{
79 title: 'Error Details',
80 value: error.message,
81 short: false
82 }, {
83 title: 'URL',
84 value: KORAP_URL,
85 short: true
86 }]
87 }]
88 });
89 } catch (slackError) {
90 console.error('Failed to send setup error to Slack:', slackError.message);
91 }
92 }
93
94 // Send notification to Nextcloud Talk
95 try {
96 await sendToNextcloudTalk(message, false);
97 } catch (ncError) {
98 console.error('Failed to send setup error to Nextcloud Talk:', ncError.message);
99 }
100
101 // Rethrow the error so that the test run is registered as failed
102 throw error;
103 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100104 })
105
Marc Kupietz93d7f702025-06-27 15:41:48 +0200106 after(async function() {
107 if (browser && typeof browser.close === 'function') {
108 await browser.close();
109 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100110 })
111
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100112 afterEach(async function () {
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100113 const testPassed = this.currentTest.state === "passed";
114 const testFailed = this.currentTest.state === "failed";
115
116 // Determine if we should send notification based on NOTIFY_ON_SUCCESS setting
117 const shouldNotify = NOTIFY_ON_SUCCESS ? testPassed : testFailed;
118
119 if (shouldNotify) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100120 // Only take screenshot if it's not one of the initial connectivity/SSL tests
121 const initialTestTitles = [
122 'should be reachable',
123 'should have a valid SSL certificate'
124 ];
125 let screenshotPath = null;
126
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100127 // Only take screenshots for failures (not for success notifications)
128 if (testFailed && !initialTestTitles.includes(this.currentTest.title) && page) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100129 screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
130 await page.screenshot({ path: screenshotPath });
131 }
132
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100133 // Prepare notification content based on success/failure
134 const emoji = testPassed ? '✅' : '🚨';
135 const status = testPassed ? 'passed' : 'failed';
136 const color = testPassed ? 'good' : 'danger';
137
Marc Kupietz65dea512025-12-04 17:55:57 +0100138 // Send notification to Slack
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200139 if (slack) {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200140 try {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200141 slack.alert({
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100142 text: `${emoji} Test on ${KORAP_URL} ${status}: *${this.currentTest.title}*`,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200143 attachments: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100144 color: color,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200145 fields: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100146 title: testPassed ? 'Passed Test' : 'Failed Test',
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200147 value: this.currentTest.title,
148 short: false
149 }, {
150 title: 'URL',
151 value: KORAP_URL,
152 short: true
153 }]
154 }]
155 });
156 } catch (slackError) {
157 console.error('Failed to send notification to Slack:', slackError.message);
158 }
159 }
160
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100161 // Upload screenshot to Slack if available (only for failures)
Marc Kupietz65dea512025-12-04 17:55:57 +0100162 if (screenshotPath) {
Marc Kupietz93d7f702025-06-27 15:41:48 +0200163 const slackToken = process.env.SLACK_TOKEN;
164 if (slackToken) {
165 try {
166 const { WebClient } = require('@slack/web-api');
Marc Kupietz65dea512025-12-04 17:55:57 +0100167 const fs = require('fs');
168 const web = new WebClient(slackToken);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200169 const channelId = process.env.SLACK_CHANNEL_ID || 'C07CM4JS48H';
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200170
Marc Kupietz93d7f702025-06-27 15:41:48 +0200171 const result = await web.files.uploadV2({
172 channel_id: channelId,
173 file: fs.createReadStream(screenshotPath),
174 filename: screenshotPath,
175 title: `Screenshot: ${this.currentTest.title}`,
176 initial_comment: `📸 Screenshot of failed test: ${this.currentTest.title} on ${KORAP_URL}`
177 });
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200178
Marc Kupietz93d7f702025-06-27 15:41:48 +0200179 } catch (uploadError) {
180 console.error('Failed to upload screenshot to Slack:', uploadError.message);
181 }
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200182 }
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200183 }
Marc Kupietz65dea512025-12-04 17:55:57 +0100184
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100185 // Send notification to Nextcloud Talk with screenshot (if available)
Marc Kupietz2f17a762025-12-06 11:45:49 +0100186 try {
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100187 const message = `${emoji} Test on ${KORAP_URL} ${status}: **${this.currentTest.title}**`;
Marc Kupietz2f17a762025-12-06 11:45:49 +0100188 await sendToNextcloudTalk(message, false, screenshotPath);
189 } catch (ncError) {
190 console.error('Failed to send notification to Nextcloud Talk:', ncError.message);
Marc Kupietz65dea512025-12-04 17:55:57 +0100191 }
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100192 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200193 })
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100194
Marc Kupietz93d7f702025-06-27 15:41:48 +0200195 it('should be reachable', function (done) {
196 let doneCalled = false;
197 const url = new URL(KORAP_URL);
198 const httpModule = url.protocol === 'https:' ? https : require('http');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100199
Marc Kupietz93d7f702025-06-27 15:41:48 +0200200 const req = httpModule.request({
201 method: 'HEAD',
202 hostname: url.hostname,
203 port: url.port || (url.protocol === 'https:' ? 443 : 80),
204 path: url.pathname,
205 timeout: 5000
206 }, res => {
207 if (!doneCalled) {
208 doneCalled = true;
209 if (res.statusCode >= 200 && res.statusCode < 400) {
210 done();
211 } else {
212 done(new Error(`Server is not reachable. Status code: ${res.statusCode}`));
213 }
214 }
215 });
216 req.on('timeout', () => {
217 if (!doneCalled) {
218 doneCalled = true;
219 req.destroy();
220 done(new Error('Request to server timed out.'));
221 }
222 });
223 req.on('error', err => {
224 if (!doneCalled) {
225 doneCalled = true;
226 done(err);
227 }
228 });
229 req.end();
230 });
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100231
Marc Kupietz93d7f702025-06-27 15:41:48 +0200232 it('should have a valid SSL certificate', function (done) {
233 let doneCalled = false;
234 const url = new URL(KORAP_URL);
235 if (url.protocol !== 'https:') {
236 return this.skip();
237 }
238 const req = https.request({
239 method: 'HEAD',
240 hostname: url.hostname,
241 port: url.port || 443,
242 path: url.pathname,
243 timeout: 5000
244 }, res => {
245 if (!doneCalled) {
246 doneCalled = true;
247 const cert = res.socket.getPeerCertificate();
248 if (cert && cert.valid_to) {
249 const validTo = new Date(cert.valid_to);
250 if (validTo > new Date()) {
251 done();
252 } else {
253 done(new Error(`SSL certificate expired on ${validTo.toDateString()}`));
254 }
255 } else if (res.socket.isSessionReused()){
256 done();
257 }
258 else {
259 done(new Error('Could not retrieve SSL certificate information.'));
260 }
261 }
262 });
263 req.on('timeout', () => {
264 if (!doneCalled) {
265 doneCalled = true;
266 req.destroy();
267 done(new Error('Request to server timed out.'));
268 }
269 });
270 req.on('error', err => {
271 if (!doneCalled) {
272 doneCalled = true;
273 if (err.code === 'CERT_HAS_EXPIRED') {
274 done(new Error('SSL certificate has expired.'));
275 } else {
276 done(err);
277 }
278 }
279 });
280 req.end();
281 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100282
Marc Kupietz93d7f702025-06-27 15:41:48 +0200283 describe('UI Tests', function() {
Marc Kupietzc4077822022-12-03 15:32:40 +0100284
Marc Kupietz93d7f702025-06-27 15:41:48 +0200285 before(function() {
286 // Check the state of the parent suite's tests
287 const initialTests = this.test.parent.parent.tests;
288 if (initialTests[0].state === 'failed' || initialTests[1].state === 'failed') {
289 this.skip();
290 }
291 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100292
Marc Kupietz93d7f702025-06-27 15:41:48 +0200293
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200294
Marc Kupietz93d7f702025-06-27 15:41:48 +0200295 it('KorAP UI is up and running', async function () {
296 try {
297 await page.goto(KORAP_URL, { waitUntil: 'domcontentloaded' });
298 await page.waitForSelector("#q-field", { visible: true });
299 const query_field = await page.$("#q-field")
300 assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
301 } catch (error) {
302 throw new Error(`Failed to load KorAP UI or find query field: ${error.message}`);
303 }
Marc Kupietz0f6c54d2022-12-03 15:32:40 +0100304 })
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100305
Marc Kupietzc4077822022-12-03 15:32:40 +0100306
Marc Kupietz93d7f702025-06-27 15:41:48 +0200307 ifConditionIt('Login into KorAP with incorrect credentials fails',
308 KORAP_LOGIN != "",
309 (async () => {
310 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD + "*")
311 login_result.should.be.false
312 }))
313
314 ifConditionIt('Login into KorAP with correct credentials succeeds',
315 KORAP_LOGIN != "",
316 (async () => {
317 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
318 login_result.should.be.true
319 }))
320
321 it('Can turn glimpse off',
322 (async () => {
323 await korap_rc.assure_glimpse_off(page)
324 }))
325
326 it('Corpus statistics show sufficient tokens',
327 (async () => {
328 const tokenCount = await korap_rc.check_corpus_statistics(page, KORAP_MIN_TOKENS_IN_CORPUS);
329 console.log(`Found ${tokenCount} tokens in corpus, minimum required: ${KORAP_MIN_TOKENS_IN_CORPUS}`);
330 tokenCount.should.be.above(KORAP_MIN_TOKENS_IN_CORPUS - 1,
331 `Corpus should have at least ${KORAP_MIN_TOKENS_IN_CORPUS} tokens, but found ${tokenCount}`);
332 })).timeout(90000)
333
334 describe('Running searches that should have hits', () => {
335
Marc Kupietz6329cac2026-06-15 16:39:55 +0200336 // The searches must run while logged in — most corpora are only
337 // available after authentication. Re-assert the session here and,
338 // when a login is required, fail loudly if it didn't take instead
339 // of silently testing the (tiny) public corpus logged out.
340 before(async () => {
341 const logged_in = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
342 if (KORAP_LOGIN != "") {
343 assert.isTrue(logged_in,
344 "Login is required for the has-hits searches but did not succeed; aborting so we don't test the public corpus while logged out")
345 }
346 })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200347
348 KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
349 it('Search for "' + query + '" has hits',
350 (async () => {
351 await korap_rc.assure_glimpse_off(page)
Marc Kupietz6329cac2026-06-15 16:39:55 +0200352 const hits = await korap_rc.search(page, query, { timeout: KORAP_SEARCH_TIMEOUT, vc: KORAP_VC })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200353 hits.should.be.above(0)
Marc Kupietz6329cac2026-06-15 16:39:55 +0200354 })).timeout(KORAP_SEARCH_TIMEOUT + 30000)
Marc Kupietz93d7f702025-06-27 15:41:48 +0200355 })
356 })
357
Marc Kupietz6329cac2026-06-15 16:39:55 +0200358 // Logout must be the LAST UI test. Mocha runs a suite's direct it()
359 // tests before its nested describe() suites, so a top-level "Logout"
360 // test would otherwise run *before* the searches above and log us out,
361 // leaving them to query the public corpus. Wrapping it in its own suite
362 // declared after the searches suite guarantees it runs last.
363 describe('Logging out', () => {
364 ifConditionIt('Logout works',
365 KORAP_LOGIN != "",
366 (async () => {
367 const logout_result = await korap_rc.logout(page)
368 logout_result.should.be.true
369 })).timeout(15000)
370 })
Marc Kupietz93d7f702025-06-27 15:41:48 +0200371 });
372});