blob: 621b22f43ba38399e6f07c333e01451eb01b4e58 [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 Kupietzbbe6de02026-02-04 09:39:48 +010023const NOTIFY_ON_SUCCESS = process.env.NOTIFY_ON_SUCCESS === 'true' || process.env.NOTIFY_ON_SUCCESS === '1';
Marc Kupietz819a52d2026-03-21 09:41:28 +010024const KORAP_HEADLESS = !(process.env.KORAP_HEADLESS === 'false' || process.env.KORAP_HEADLESS === '0');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010025const korap_rc = require('../lib/korap_rc.js').new(KORAP_URL)
Marc Kupietz2f17a762025-12-06 11:45:49 +010026const { sendToNextcloudTalk, ifConditionIt } = require('../lib/utils.js');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010027
Marc Kupietz7f1666a2024-07-12 18:35:31 +020028const slack_webhook = process.env.SLACK_WEBHOOK_URL;
Marc Kupietz4d335a32024-09-04 16:13:48 +020029
Marc Kupietz7f1666a2024-07-12 18:35:31 +020030if (slack_webhook) {
31 slack = require('slack-notify')(slack_webhook);
32}
33
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010034
Marc Kupietzc4077822022-12-03 15:32:40 +010035
Marc Kupietz0f6c54d2022-12-03 15:32:40 +010036describe('Running KorAP UI end-to-end tests on ' + KORAP_URL, () => {
Marc Kupietzc4077822022-12-03 15:32:40 +010037
Marc Kupietz93d7f702025-06-27 15:41:48 +020038 let browser;
39 let page;
40
41
Marc Kupietzc4077822022-12-03 15:32:40 +010042 before(async () => {
Marc Kupietzb4d62a82026-06-04 14:52:28 +020043 try {
44 browser = await puppeteer.launch({
45 headless: KORAP_HEADLESS ? "shell" : false,
46 args: [
47 '--no-sandbox',
48 '--disable-setuid-sandbox',
49 '--disable-dev-shm-usage',
50 '--disable-accelerated-2d-canvas',
51 '--no-first-run',
52 '--no-zygote',
53 '--disable-gpu'
54 ]
55 })
56 page = await browser.newPage()
57 await page.setViewport({
58 width: 1980,
59 height: 768,
60 deviceScaleFactor: 1,
61 });
62 } catch (error) {
63 console.error('Failed to initialize Puppeteer browser:', error.message);
64
65 // Send failure notification
66 const emoji = '🚨';
67 const message = `${emoji} Test setup on ${KORAP_URL} failed: **${error.message}**`;
68
69 // Send notification to Slack
70 if (slack) {
71 try {
72 slack.alert({
73 text: `${emoji} Test setup on ${KORAP_URL} failed`,
74 attachments: [{
75 color: 'danger',
76 fields: [{
77 title: 'Error Details',
78 value: error.message,
79 short: false
80 }, {
81 title: 'URL',
82 value: KORAP_URL,
83 short: true
84 }]
85 }]
86 });
87 } catch (slackError) {
88 console.error('Failed to send setup error to Slack:', slackError.message);
89 }
90 }
91
92 // Send notification to Nextcloud Talk
93 try {
94 await sendToNextcloudTalk(message, false);
95 } catch (ncError) {
96 console.error('Failed to send setup error to Nextcloud Talk:', ncError.message);
97 }
98
99 // Rethrow the error so that the test run is registered as failed
100 throw error;
101 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100102 })
103
Marc Kupietz93d7f702025-06-27 15:41:48 +0200104 after(async function() {
105 if (browser && typeof browser.close === 'function') {
106 await browser.close();
107 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100108 })
109
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100110 afterEach(async function () {
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100111 const testPassed = this.currentTest.state === "passed";
112 const testFailed = this.currentTest.state === "failed";
113
114 // Determine if we should send notification based on NOTIFY_ON_SUCCESS setting
115 const shouldNotify = NOTIFY_ON_SUCCESS ? testPassed : testFailed;
116
117 if (shouldNotify) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100118 // Only take screenshot if it's not one of the initial connectivity/SSL tests
119 const initialTestTitles = [
120 'should be reachable',
121 'should have a valid SSL certificate'
122 ];
123 let screenshotPath = null;
124
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100125 // Only take screenshots for failures (not for success notifications)
126 if (testFailed && !initialTestTitles.includes(this.currentTest.title) && page) {
Marc Kupietz65dea512025-12-04 17:55:57 +0100127 screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
128 await page.screenshot({ path: screenshotPath });
129 }
130
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100131 // Prepare notification content based on success/failure
132 const emoji = testPassed ? '✅' : '🚨';
133 const status = testPassed ? 'passed' : 'failed';
134 const color = testPassed ? 'good' : 'danger';
135
Marc Kupietz65dea512025-12-04 17:55:57 +0100136 // Send notification to Slack
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200137 if (slack) {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200138 try {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200139 slack.alert({
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100140 text: `${emoji} Test on ${KORAP_URL} ${status}: *${this.currentTest.title}*`,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200141 attachments: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100142 color: color,
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200143 fields: [{
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100144 title: testPassed ? 'Passed Test' : 'Failed Test',
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200145 value: this.currentTest.title,
146 short: false
147 }, {
148 title: 'URL',
149 value: KORAP_URL,
150 short: true
151 }]
152 }]
153 });
154 } catch (slackError) {
155 console.error('Failed to send notification to Slack:', slackError.message);
156 }
157 }
158
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100159 // Upload screenshot to Slack if available (only for failures)
Marc Kupietz65dea512025-12-04 17:55:57 +0100160 if (screenshotPath) {
Marc Kupietz93d7f702025-06-27 15:41:48 +0200161 const slackToken = process.env.SLACK_TOKEN;
162 if (slackToken) {
163 try {
164 const { WebClient } = require('@slack/web-api');
Marc Kupietz65dea512025-12-04 17:55:57 +0100165 const fs = require('fs');
166 const web = new WebClient(slackToken);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200167 const channelId = process.env.SLACK_CHANNEL_ID || 'C07CM4JS48H';
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200168
Marc Kupietz93d7f702025-06-27 15:41:48 +0200169 const result = await web.files.uploadV2({
170 channel_id: channelId,
171 file: fs.createReadStream(screenshotPath),
172 filename: screenshotPath,
173 title: `Screenshot: ${this.currentTest.title}`,
174 initial_comment: `📸 Screenshot of failed test: ${this.currentTest.title} on ${KORAP_URL}`
175 });
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200176
Marc Kupietz93d7f702025-06-27 15:41:48 +0200177 } catch (uploadError) {
178 console.error('Failed to upload screenshot to Slack:', uploadError.message);
179 }
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200180 }
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200181 }
Marc Kupietz65dea512025-12-04 17:55:57 +0100182
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100183 // Send notification to Nextcloud Talk with screenshot (if available)
Marc Kupietz2f17a762025-12-06 11:45:49 +0100184 try {
Marc Kupietzbbe6de02026-02-04 09:39:48 +0100185 const message = `${emoji} Test on ${KORAP_URL} ${status}: **${this.currentTest.title}**`;
Marc Kupietz2f17a762025-12-06 11:45:49 +0100186 await sendToNextcloudTalk(message, false, screenshotPath);
187 } catch (ncError) {
188 console.error('Failed to send notification to Nextcloud Talk:', ncError.message);
Marc Kupietz65dea512025-12-04 17:55:57 +0100189 }
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100190 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200191 })
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100192
Marc Kupietz93d7f702025-06-27 15:41:48 +0200193 it('should be reachable', function (done) {
194 let doneCalled = false;
195 const url = new URL(KORAP_URL);
196 const httpModule = url.protocol === 'https:' ? https : require('http');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100197
Marc Kupietz93d7f702025-06-27 15:41:48 +0200198 const req = httpModule.request({
199 method: 'HEAD',
200 hostname: url.hostname,
201 port: url.port || (url.protocol === 'https:' ? 443 : 80),
202 path: url.pathname,
203 timeout: 5000
204 }, res => {
205 if (!doneCalled) {
206 doneCalled = true;
207 if (res.statusCode >= 200 && res.statusCode < 400) {
208 done();
209 } else {
210 done(new Error(`Server is not reachable. Status code: ${res.statusCode}`));
211 }
212 }
213 });
214 req.on('timeout', () => {
215 if (!doneCalled) {
216 doneCalled = true;
217 req.destroy();
218 done(new Error('Request to server timed out.'));
219 }
220 });
221 req.on('error', err => {
222 if (!doneCalled) {
223 doneCalled = true;
224 done(err);
225 }
226 });
227 req.end();
228 });
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100229
Marc Kupietz93d7f702025-06-27 15:41:48 +0200230 it('should have a valid SSL certificate', function (done) {
231 let doneCalled = false;
232 const url = new URL(KORAP_URL);
233 if (url.protocol !== 'https:') {
234 return this.skip();
235 }
236 const req = https.request({
237 method: 'HEAD',
238 hostname: url.hostname,
239 port: url.port || 443,
240 path: url.pathname,
241 timeout: 5000
242 }, res => {
243 if (!doneCalled) {
244 doneCalled = true;
245 const cert = res.socket.getPeerCertificate();
246 if (cert && cert.valid_to) {
247 const validTo = new Date(cert.valid_to);
248 if (validTo > new Date()) {
249 done();
250 } else {
251 done(new Error(`SSL certificate expired on ${validTo.toDateString()}`));
252 }
253 } else if (res.socket.isSessionReused()){
254 done();
255 }
256 else {
257 done(new Error('Could not retrieve SSL certificate information.'));
258 }
259 }
260 });
261 req.on('timeout', () => {
262 if (!doneCalled) {
263 doneCalled = true;
264 req.destroy();
265 done(new Error('Request to server timed out.'));
266 }
267 });
268 req.on('error', err => {
269 if (!doneCalled) {
270 doneCalled = true;
271 if (err.code === 'CERT_HAS_EXPIRED') {
272 done(new Error('SSL certificate has expired.'));
273 } else {
274 done(err);
275 }
276 }
277 });
278 req.end();
279 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100280
Marc Kupietz93d7f702025-06-27 15:41:48 +0200281 describe('UI Tests', function() {
Marc Kupietzc4077822022-12-03 15:32:40 +0100282
Marc Kupietz93d7f702025-06-27 15:41:48 +0200283 before(function() {
284 // Check the state of the parent suite's tests
285 const initialTests = this.test.parent.parent.tests;
286 if (initialTests[0].state === 'failed' || initialTests[1].state === 'failed') {
287 this.skip();
288 }
289 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100290
Marc Kupietz93d7f702025-06-27 15:41:48 +0200291
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200292
Marc Kupietz93d7f702025-06-27 15:41:48 +0200293 it('KorAP UI is up and running', async function () {
294 try {
295 await page.goto(KORAP_URL, { waitUntil: 'domcontentloaded' });
296 await page.waitForSelector("#q-field", { visible: true });
297 const query_field = await page.$("#q-field")
298 assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
299 } catch (error) {
300 throw new Error(`Failed to load KorAP UI or find query field: ${error.message}`);
301 }
Marc Kupietz0f6c54d2022-12-03 15:32:40 +0100302 })
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100303
Marc Kupietzc4077822022-12-03 15:32:40 +0100304
Marc Kupietz93d7f702025-06-27 15:41:48 +0200305 ifConditionIt('Login into KorAP with incorrect credentials fails',
306 KORAP_LOGIN != "",
307 (async () => {
308 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD + "*")
309 login_result.should.be.false
310 }))
311
312 ifConditionIt('Login into KorAP with correct credentials succeeds',
313 KORAP_LOGIN != "",
314 (async () => {
315 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
316 login_result.should.be.true
317 }))
318
319 it('Can turn glimpse off',
320 (async () => {
321 await korap_rc.assure_glimpse_off(page)
322 }))
323
324 it('Corpus statistics show sufficient tokens',
325 (async () => {
326 const tokenCount = await korap_rc.check_corpus_statistics(page, KORAP_MIN_TOKENS_IN_CORPUS);
327 console.log(`Found ${tokenCount} tokens in corpus, minimum required: ${KORAP_MIN_TOKENS_IN_CORPUS}`);
328 tokenCount.should.be.above(KORAP_MIN_TOKENS_IN_CORPUS - 1,
329 `Corpus should have at least ${KORAP_MIN_TOKENS_IN_CORPUS} tokens, but found ${tokenCount}`);
330 })).timeout(90000)
331
332 describe('Running searches that should have hits', () => {
333
334 before(async () => { await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD) })
335
336 KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
337 it('Search for "' + query + '" has hits',
338 (async () => {
339 await korap_rc.assure_glimpse_off(page)
340 const hits = await korap_rc.search(page, query)
341 hits.should.be.above(0)
342 })).timeout(20000)
343 })
344 })
345
346 ifConditionIt('Logout works',
347 KORAP_LOGIN != "",
348 (async () => {
349 const logout_result = await korap_rc.logout(page)
350 logout_result.should.be.true
351 })).timeout(15000)
352 });
353});