blob: b7341112172eee38eb4ed754117d747906684594 [file] [log] [blame]
const https = require('https');
const puppeteer = require('puppeteer-extra');
puppeteer.use(require('puppeteer-extra-plugin-user-preferences')({
userPrefs: {
safebrowsing: {
enabled: false,
enhanced: false
}
}
}));
const chai = require('chai');
const { afterEach } = require('mocha');
const assert = chai.assert;
const should = chai.should();
var slack = null;
const KORAP_URL = process.env.KORAP_URL || "http://localhost:64543";
const KORAP_LOGIN = 'KORAP_USERNAME' in process.env ? process.env.KORAP_USERNAME : 'KORAP_LOGIN' in process.env ? process.env.KORAP_LOGIN : "user2"
const KORAP_PWD = process.env.KORAP_PWD || process.env.KORAP_PASSWORD || "password2";
const KORAP_QUERIES = process.env.KORAP_QUERIES || 'geht, [orth=geht & cmc/pos=VVFIN]'
const KORAP_MIN_TOKENS_IN_CORPUS = parseInt(process.env.KORAP_MIN_TOKENS_IN_CORPUS || "100000", 10);
const KORAP_SEARCH_TIMEOUT = parseInt(process.env.KORAP_SEARCH_TIMEOUT || "60000", 10);
const KORAP_VC = process.env.KORAP_VC || process.env.VC || "";
const NOTIFY_ON_SUCCESS = process.env.NOTIFY_ON_SUCCESS === 'true' || process.env.NOTIFY_ON_SUCCESS === '1';
const KORAP_DISABLE_GLIMPSE = process.env.KORAP_DISABLE_GLIMPSE === 'true' || process.env.KORAP_DISABLE_GLIMPSE === '1';
const KORAP_HEADLESS = !(process.env.KORAP_HEADLESS === 'false' || process.env.KORAP_HEADLESS === '0');
const korap_rc = require('../lib/korap_rc.js').new(KORAP_URL)
const { sendToNextcloudTalk, ifConditionIt } = require('../lib/utils.js');
const slack_webhook = process.env.SLACK_WEBHOOK_URL;
if (slack_webhook) {
slack = require('slack-notify')(slack_webhook);
}
describe('Running KorAP UI end-to-end tests on ' + KORAP_URL, () => {
let browser;
let page;
before(async () => {
try {
browser = await puppeteer.launch({
headless: KORAP_HEADLESS ? "shell" : false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu'
]
})
page = await browser.newPage()
await page.setViewport({
width: 1980,
height: 768,
deviceScaleFactor: 1,
});
console.log(`Run started ${new Date().toISOString()} on ${KORAP_URL}` +
(KORAP_VC ? ` (virtual corpus: ${KORAP_VC})` : ''));
} catch (error) {
console.error('Failed to initialize Puppeteer browser:', error.message);
// Send failure notification
const emoji = '🚨';
const message = `${emoji} Test setup on ${KORAP_URL} failed: **${error.message}**`;
// Send notification to Slack
if (slack) {
try {
slack.alert({
text: `${emoji} Test setup on ${KORAP_URL} failed`,
attachments: [{
color: 'danger',
fields: [{
title: 'Error Details',
value: error.message,
short: false
}, {
title: 'URL',
value: KORAP_URL,
short: true
}]
}]
});
} catch (slackError) {
console.error('Failed to send setup error to Slack:', slackError.message);
}
}
// Send notification to Nextcloud Talk
try {
await sendToNextcloudTalk(message, false);
} catch (ncError) {
console.error('Failed to send setup error to Nextcloud Talk:', ncError.message);
}
// Rethrow the error so that the test run is registered as failed
throw error;
}
})
after(async function() {
if (browser && typeof browser.close === 'function') {
await browser.close();
}
})
afterEach(async function () {
const testPassed = this.currentTest.state === "passed";
const testFailed = this.currentTest.state === "failed";
// Determine if we should send notification based on NOTIFY_ON_SUCCESS setting
const shouldNotify = NOTIFY_ON_SUCCESS ? testPassed : testFailed;
if (shouldNotify) {
// Only take screenshot if it's not one of the initial connectivity/SSL tests
const initialTestTitles = [
'should be reachable',
'should have a valid SSL certificate'
];
let screenshotPath = null;
// Only take screenshots for failures (not for success notifications)
if (testFailed && !initialTestTitles.includes(this.currentTest.title) && page) {
screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
await page.screenshot({ path: screenshotPath });
}
// Prepare notification content based on success/failure
const emoji = testPassed ? '✅' : '🚨';
const status = testPassed ? 'passed' : 'failed';
const color = testPassed ? 'good' : 'danger';
const timestamp = new Date().toISOString();
// Capture the actual page URL (e.g. the full search results URL with
// query and cq) so issues can be reproduced/traced immediately. Fall
// back to the instance URL for tests that never navigated (about:blank).
let currentUrl = KORAP_URL;
try {
const u = page && typeof page.url === 'function' ? page.url() : '';
if (/^https?:/.test(u)) currentUrl = u;
} catch (e) { /* keep KORAP_URL */ }
// Echo the result with its URL to the console log as well.
console.log(`${emoji} [${timestamp}] ${status}: ${this.currentTest.title} — ${currentUrl}`);
// Send notification to Slack
if (slack) {
try {
slack.alert({
text: `${emoji} [${timestamp}] Test on ${KORAP_URL} ${status}: *${this.currentTest.title}*`,
attachments: [{
color: color,
fields: [{
title: testPassed ? 'Passed Test' : 'Failed Test',
value: this.currentTest.title,
short: false
}, {
title: 'URL',
value: currentUrl,
short: false
}]
}]
});
} catch (slackError) {
console.error('Failed to send notification to Slack:', slackError.message);
}
}
// Upload screenshot to Slack if available (only for failures)
if (screenshotPath) {
const slackToken = process.env.SLACK_TOKEN;
if (slackToken) {
try {
const { WebClient } = require('@slack/web-api');
const fs = require('fs');
const web = new WebClient(slackToken);
const channelId = process.env.SLACK_CHANNEL_ID || 'C07CM4JS48H';
const result = await web.files.uploadV2({
channel_id: channelId,
file: fs.createReadStream(screenshotPath),
filename: screenshotPath,
title: `Screenshot: ${this.currentTest.title}`,
initial_comment: `📸 Screenshot of failed test: ${this.currentTest.title} on ${KORAP_URL}`
});
} catch (uploadError) {
console.error('Failed to upload screenshot to Slack:', uploadError.message);
}
}
}
// Send notification to Nextcloud Talk with screenshot (if available)
try {
const message = `${emoji} [${timestamp}] Test on ${KORAP_URL} ${status}: **${this.currentTest.title}**\n${currentUrl}`;
await sendToNextcloudTalk(message, false, screenshotPath);
} catch (ncError) {
console.error('Failed to send notification to Nextcloud Talk:', ncError.message);
}
}
})
it('should be reachable', function (done) {
let doneCalled = false;
const url = new URL(KORAP_URL);
const httpModule = url.protocol === 'https:' ? https : require('http');
const req = httpModule.request({
method: 'HEAD',
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
timeout: 5000
}, res => {
if (!doneCalled) {
doneCalled = true;
if (res.statusCode >= 200 && res.statusCode < 400) {
done();
} else {
done(new Error(`Server is not reachable. Status code: ${res.statusCode}`));
}
}
});
req.on('timeout', () => {
if (!doneCalled) {
doneCalled = true;
req.destroy();
done(new Error('Request to server timed out.'));
}
});
req.on('error', err => {
if (!doneCalled) {
doneCalled = true;
done(err);
}
});
req.end();
});
it('should have a valid SSL certificate', function (done) {
let doneCalled = false;
const url = new URL(KORAP_URL);
if (url.protocol !== 'https:') {
return this.skip();
}
const req = https.request({
method: 'HEAD',
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
timeout: 5000
}, res => {
if (!doneCalled) {
doneCalled = true;
const cert = res.socket.getPeerCertificate();
if (cert && cert.valid_to) {
const validTo = new Date(cert.valid_to);
if (validTo > new Date()) {
done();
} else {
done(new Error(`SSL certificate expired on ${validTo.toDateString()}`));
}
} else if (res.socket.isSessionReused()){
done();
}
else {
done(new Error('Could not retrieve SSL certificate information.'));
}
}
});
req.on('timeout', () => {
if (!doneCalled) {
doneCalled = true;
req.destroy();
done(new Error('Request to server timed out.'));
}
});
req.on('error', err => {
if (!doneCalled) {
doneCalled = true;
if (err.code === 'CERT_HAS_EXPIRED') {
done(new Error('SSL certificate has expired.'));
} else {
done(err);
}
}
});
req.end();
});
describe('UI Tests', function() {
before(function() {
// Check the state of the parent suite's tests
const initialTests = this.test.parent.parent.tests;
if (initialTests[0].state === 'failed' || initialTests[1].state === 'failed') {
this.skip();
}
});
it('KorAP UI is up and running', async function () {
try {
await page.goto(KORAP_URL, { waitUntil: 'domcontentloaded' });
await page.waitForSelector("#q-field", { visible: true });
const query_field = await page.$("#q-field")
assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
} catch (error) {
throw new Error(`Failed to load KorAP UI or find query field: ${error.message}`);
}
})
ifConditionIt('Login into KorAP with incorrect credentials fails',
KORAP_LOGIN != "",
(async () => {
const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD + "*")
login_result.should.be.false
}))
ifConditionIt('Login into KorAP with correct credentials succeeds',
KORAP_LOGIN != "",
(async () => {
const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
login_result.should.be.true
}))
it('Corpus statistics show sufficient tokens',
(async () => {
const tokenCount = await korap_rc.check_corpus_statistics(page, KORAP_MIN_TOKENS_IN_CORPUS);
console.log(`Found ${tokenCount} tokens in corpus, minimum required: ${KORAP_MIN_TOKENS_IN_CORPUS}`);
tokenCount.should.be.above(KORAP_MIN_TOKENS_IN_CORPUS - 1,
`Corpus should have at least ${KORAP_MIN_TOKENS_IN_CORPUS} tokens, but found ${tokenCount}`);
})).timeout(90000)
describe('Running searches that should have hits', () => {
// The searches must run while logged in — most corpora are only
// available after authentication. Re-assert the session here and,
// when a login is required, fail loudly if it didn't take instead
// of silently testing the (tiny) public corpus logged out.
before(async () => {
const logged_in = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
if (KORAP_LOGIN != "") {
assert.isTrue(logged_in,
"Login is required for the has-hits searches but did not succeed; aborting so we don't test the public corpus while logged out")
}
})
const vcLabel = KORAP_VC ? ` in vc "${KORAP_VC}"` : '';
KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
it('Search for "' + query + '"' + vcLabel + ' has hits',
(async () => {
// Leave glimpse (cutoff) on by default to reduce server load; it
// still proves the server is up. Set KORAP_DISABLE_GLIMPSE=true
// to force full (uncapped) result counts if needed.
if (KORAP_DISABLE_GLIMPSE) {
await korap_rc.assure_glimpse_off(page)
}
const hits = await korap_rc.search(page, query, { timeout: KORAP_SEARCH_TIMEOUT, vc: KORAP_VC })
hits.should.be.above(0)
})).timeout(KORAP_SEARCH_TIMEOUT + 30000)
})
})
// Logout must be the LAST UI test. Mocha runs a suite's direct it()
// tests before its nested describe() suites, so a top-level "Logout"
// test would otherwise run *before* the searches above and log us out,
// leaving them to query the public corpus. Wrapping it in its own suite
// declared after the searches suite guarantees it runs last.
describe('Logging out', () => {
ifConditionIt('Logout works',
KORAP_LOGIN != "",
(async () => {
const logout_result = await korap_rc.logout(page)
logout_result.should.be.true
})).timeout(15000)
})
});
});