First test if server is reachable and via https

Change-Id: I02bdb64e0300d2d7913e3929505eb2e5e29f79ea
diff --git a/lib/korap_rc.js b/lib/korap_rc.js
index 21c1eb8..8e5a22f 100644
--- a/lib/korap_rc.js
+++ b/lib/korap_rc.js
@@ -13,123 +13,132 @@
     }
 
     async login(page, username, password) {
-        page.goto(this.korap_url);
-        await page.waitForNavigation({ waitUntil: 'networkidle2' });
-        if (this.login == "") return false;
-        if (username == "") return false;
-        if (password == "") return false;
+        try {
+            await page.goto(this.korap_url, { waitUntil: 'domcontentloaded' });
+            if (username == "") return false;
+            if (password == "") return false;
 
-        await page.click('.dropdown-btn');
-        await page.waitForSelector('input[name=handle_or_email]', { visible: true });
-        const username_field = await page.$("input[name=handle_or_email]")
-        if (username_field != null) {
-            await username_field.focus();
-            await username_field.type(username);
-            const password_field = await page.$("input[name=pwd]")
-            await password_field.focus()
-            await page.keyboard.type(password)
-            await page.keyboard.press("Enter")
-        } else {
-            return false
+            await page.waitForSelector('.dropdown-btn', { visible: true });
+            await page.click('.dropdown-btn');
+            await page.waitForSelector('input[name=handle_or_email]', { visible: true });
+            const username_field = await page.$("input[name=handle_or_email]")
+            if (username_field != null) {
+                await username_field.focus();
+                await username_field.type(username);
+                const password_field = await page.$("input[name=pwd]")
+                await password_field.focus()
+                await page.keyboard.type(password)
+                await page.keyboard.press("Enter")
+            } else {
+                return false
+            }
+
+            await page.waitForNavigation({ waitUntil: 'domcontentloaded' }); // Wait for navigation after login
+            await page.waitForSelector("#q-field", { visible: true }); // Wait for query field to confirm login
+            const logout = await page.$(".logout")
+            if (logout == null) {
+                return false
+            }
+
+            return true
+        } catch (error) {
+            console.error(`Login failed: ${error.message}`);
+            return false;
         }
-
-        await page.waitForNavigation({ waitUntil: 'networkidle2' });
-        const logout = await page.$(".logout")
-        if (logout == null) {
-            return false
-        }
-
-        return true
     }
 
     async search(page, query) {
-        const query_field = await page.$("#q-field");
-        assert.notEqual(query_field, null, "Query field not found");
-
-        await page.waitForSelector("#q-field", { visible: true });
-        await query_field.click({ clickCount: 3 });
-        await page.keyboard.type(query);
-        await page.keyboard.press("Enter");
-
-        await page.waitForNavigation({ waitUntil: 'networkidle2' });
-
-        // Wait for search results to be fully loaded
         try {
-            await page.waitForSelector('ol li, #resultinfo, .result-item', {
-                visible: true,
-                timeout: 15000
-            });
-            // Give additional time for the results count to be populated
-            await new Promise(resolve => setTimeout(resolve, 2000));
-        } catch (error) {
-            // Continue if timeout, fallback methods will handle it
-        }
+            await page.waitForSelector("#q-field", { visible: true });
+            const query_field = await page.$("#q-field");
+            assert.notEqual(query_field, null, "Query field not found");
 
-        const resultsInfo = await page.evaluate(() => {
-            // Check common selectors for result counts
-            const selectors = [
-                '#total-results',
-                '#resultinfo',
-                '.result-count',
-                '.total-results',
-                '[data-results]',
-                '.found'
-            ];
+            await query_field.click({ clickCount: 3 });
+            await page.keyboard.type(query);
+            await page.keyboard.press("Enter");
 
-            for (const selector of selectors) {
-                const element = document.querySelector(selector);
-                if (element) {
-                    const text = element.textContent || element.innerText || '';
-                    const numbers = text.match(/\d+/g);
+            await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
+
+            // Wait for search results to be fully loaded
+            try {
+                await page.waitForSelector('ol li, #resultinfo, .result-item', {
+                    visible: true,
+                    timeout: 15000
+                });
+                // Give additional time for the results count to be populated
+                await new Promise(resolve => setTimeout(resolve, 2000));
+            } catch (error) {
+                // Continue if timeout, fallback methods will handle it
+            }
+
+            const resultsInfo = await page.evaluate(() => {
+                // Check common selectors for result counts
+                const selectors = [
+                    '#total-results',
+                    '#resultinfo',
+                    '.result-count',
+                    '.total-results',
+                    '[data-results]',
+                    '.found'
+                ];
+
+                for (const selector of selectors) {
+                    const element = document.querySelector(selector);
+                    if (element) {
+                        const text = element.textContent || element.innerText || '';
+                        const numbers = text.match(/\d+/g);
+                        if (numbers && numbers.length > 0) {
+                            return {
+                                selector: selector,
+                                numbers: numbers
+                            };
+                        }
+                    }
+                }
+
+                // Look in the page title for results count
+                const title = document.title;
+                if (title) {
+                    const numbers = title.match(/\d+/g);
                     if (numbers && numbers.length > 0) {
                         return {
-                            selector: selector,
+                            selector: 'title',
                             numbers: numbers
                         };
                     }
                 }
-            }
 
-            // Look in the page title for results count
-            const title = document.title;
-            if (title) {
-                const numbers = title.match(/\d+/g);
-                if (numbers && numbers.length > 0) {
+                // Count the actual result items as fallback
+                const resultItems = document.querySelectorAll('ol li');
+                if (resultItems.length > 0) {
                     return {
-                        selector: 'title',
-                        numbers: numbers
+                        selector: 'counted-items',
+                        numbers: [resultItems.length.toString()]
                     };
                 }
-            }
 
-            // Count the actual result items as fallback
-            const resultItems = document.querySelectorAll('ol li');
-            if (resultItems.length > 0) {
-                return {
-                    selector: 'counted-items',
-                    numbers: [resultItems.length.toString()]
-                };
-            }
-
-            return null;
-        });
-
-        if (!resultsInfo || !resultsInfo.numbers || resultsInfo.numbers.length === 0) {
-            // Final fallback: just count visible list items
-            const itemCount = await page.evaluate(() => {
-                return document.querySelectorAll('ol li').length;
+                return null;
             });
 
-            if (itemCount > 0) {
-                return itemCount;
+            if (!resultsInfo || !resultsInfo.numbers || resultsInfo.numbers.length === 0) {
+                // Final fallback: just count visible list items
+                const itemCount = await page.evaluate(() => {
+                    return document.querySelectorAll('ol li').length;
+                });
+
+                if (itemCount > 0) {
+                    return itemCount;
+                }
+
+                throw new Error("Cannot find any results count on the page");
             }
 
-            throw new Error("Cannot find any results count on the page");
+            // Extract the largest number found (likely the total results)
+            const hits = Math.max(...resultsInfo.numbers.map(n => parseInt(n, 10)));
+            return hits;
+        } catch (error) {
+            throw new Error(`Failed to perform search: ${error.message}`);
         }
-
-        // Extract the largest number found (likely the total results)
-        const hits = Math.max(...resultsInfo.numbers.map(n => parseInt(n, 10)));
-        return hits;
     }
 
     async logout(page) {
@@ -160,24 +169,55 @@
     async check_corpus_statistics(page, minTokenThreshold = 1000) {
         try {
             // Navigate to the corpus view if not already there
-            await page.goto(this.korap_url, { waitUntil: 'networkidle2' });
+            await page.goto(this.korap_url, { waitUntil: 'domcontentloaded' });
             
             // Click the vc-choose element to open corpus selection
-            await page.waitForSelector('#vc-choose', { visible: true, timeout: 10000 });
+            await page.waitForSelector('#vc-choose', { visible: true, timeout: 90000 });
             await page.click('#vc-choose');
             
             // Wait a moment for the UI to respond
             await new Promise(resolve => setTimeout(resolve, 1000));
             
             // Click the statistic element
-            await page.waitForSelector('.statistic', { visible: true, timeout: 10000 });
-            await page.click('.statistic');
+            await page.waitForSelector('.statistic', { visible: true, timeout: 90000 });
+            try {
+                await page.click('.statistic');
+            } catch (error) {
+                throw new Error(`Failed to click statistic element: ${error.message}`);
+            }
             
-            // Wait for statistics to load
-            await new Promise(resolve => setTimeout(resolve, 3000));
-            
+            // Wait for statistics to load and token count to appear
+            await page.waitForFunction(() => {
+                const tokenTitleElements = document.querySelectorAll('[title="tokens"], [title*="token"]');
+                for (const element of tokenTitleElements) {
+                    let nextElement = element.nextElementSibling;
+                    while (nextElement) {
+                        if (nextElement.tagName.toLowerCase() === 'dd') {
+                            const text = nextElement.textContent || nextElement.innerText || '';
+                            const cleanedText = text.replace(/[,\.]/g, '');
+                            const numbers = cleanedText.match(/\d+/g);
+                            if (numbers && numbers.length > 0) {
+                                return true;
+                            }
+                        }
+                        nextElement = nextElement.nextElementSibling;
+                    }
+                }
+                const ddElements = document.querySelectorAll('dd');
+                for (const dd of ddElements) {
+                    const text = dd.textContent || dd.innerText || '';
+                    const cleanedText = text.replace(/[,\.]/g, '');
+                    const numbers = cleanedText.match(/\d+/g);
+                    if (numbers && numbers.length > 0) {
+                        return true;
+                    }
+                }
+                return false;
+            }, { timeout: 60000 });
+
             // Look for the tokens count in a dd element that follows an element with title "tokens"
             const tokenCount = await page.evaluate((minThreshold) => {
+                console.log("Attempting to find token count within page.evaluate...");
                 // Find the element with title "tokens"
                 const tokenTitleElements = document.querySelectorAll('[title="tokens"], [title*="token"]');
                 
@@ -191,6 +231,7 @@
                             const cleanedText = text.replace(/[,\.]/g, '');
                             const numbers = cleanedText.match(/\d+/g);
                             if (numbers && numbers.length > 0) {
+                                console.log(`Found token count from title element: ${numbers[0]}`);
                                 return parseInt(numbers[0], 10);
                             }
                         }
@@ -209,11 +250,13 @@
                         const num = parseInt(numbers[0], 10);
                         // Use the provided threshold instead of hardcoded value
                         if (num > minThreshold) {
+                            console.log(`Found token count from dd element: ${num}`);
                             return num;
                         }
                     }
                 }
                 
+                console.log("Could not find token count using any method.");
                 return null;
             }, minTokenThreshold);
             
@@ -229,4 +272,4 @@
     }
 }
 
-module.exports = KorAPRC
+module.exports = KorAPRC
\ No newline at end of file
diff --git a/test/korap-ui.js b/test/korap-ui.js
index f81193a..fcf1fcd 100644
--- a/test/korap-ui.js
+++ b/test/korap-ui.js
@@ -1,3 +1,4 @@
+const https = require('https');
 const puppeteer = require('puppeteer-extra');
 puppeteer.use(require('puppeteer-extra-plugin-user-preferences')({
     userPrefs: {
@@ -32,6 +33,10 @@
 
 describe('Running KorAP UI end-to-end tests on ' + KORAP_URL, () => {
 
+    let browser;
+    let page;
+    
+
     before(async () => {
         browser = await puppeteer.launch({
             headless: "shell",
@@ -51,21 +56,19 @@
             height: 768,
             deviceScaleFactor: 1,
         });
-        console.log("Browser version: " + await browser.version() + " started")
+        
     })
 
-    after(async () => {
-        await browser.close()
+    after(async function() {
+        if (browser && typeof browser.close === 'function') {
+            await browser.close();
+        }
     })
 
     afterEach(async function () {
         if (this.currentTest.state == "failed") {
-            const screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
-            await page.screenshot({ path: screenshotPath });
-
             if (slack) {
                 try {
-                    // First send text notification via webhook
                     slack.alert({
                         text: `🚨 Test on ${KORAP_URL} failed: *${this.currentTest.title}*`,
                         attachments: [{
@@ -86,87 +89,196 @@
                 }
             }
 
-            // Try to upload screenshot via Slack Web API if token is available
-            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';
+            // 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'
+            ];
+            if (!initialTestTitles.includes(this.currentTest.title) && page) {
+                const screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
+                await page.screenshot({ path: screenshotPath });
 
-                    // Upload the file to Slack
-                    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}`
-                    });
+                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';
 
-                    console.log('Screenshot uploaded to Slack successfully');
+                        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);
+                    } catch (uploadError) {
+                        console.error('Failed to upload screenshot to Slack:', uploadError.message);
+                    }
                 }
             }
         }
     })
 
-    it('KorAP UI is up and running',
-        (async () => {
-            page.goto(KORAP_URL);
-            await page.waitForNavigation({ waitUntil: 'networkidle2' });
-            const query_field = await page.$("#q-field")
-            assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
-        }))
+    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();
+    });
 
-    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
-        }))
+    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();
+    });
 
-    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
-        }))
+    describe('UI Tests', function() {
 
-    it('Can turn glimpse off',
-        (async () => {
-            await korap_rc.assure_glimpse_off(page)
-        }))
+        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('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(50000)
+        
 
-    describe('Running searches that should have hits', () => {
-
-        before(async () => { await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD) })
-
-        KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
-            it('Search for "' + query + '" has hits',
-                (async () => {
-                    await korap_rc.assure_glimpse_off(page)
-                    const hits = await korap_rc.search(page, query)
-                    hits.should.be.above(0)
-                })).timeout(20000)
+        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('Logout works',
-        KORAP_LOGIN != "",
-        (async () => {
-            const logout_result = await korap_rc.logout(page)
-            logout_result.should.be.true
-        })).timeout(15000)
 
-})
+        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('Can turn glimpse off',
+            (async () => {
+                await korap_rc.assure_glimpse_off(page)
+            }))
+
+        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', () => {
+
+            before(async () => { await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD) })
+
+            KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
+                it('Search for "' + query + '" has hits',
+                    (async () => {
+                        await korap_rc.assure_glimpse_off(page)
+                        const hits = await korap_rc.search(page, query)
+                        hits.should.be.above(0)
+                    })).timeout(20000)
+            })
+        })
+
+        ifConditionIt('Logout works',
+            KORAP_LOGIN != "",
+            (async () => {
+                const logout_result = await korap_rc.logout(page)
+                logout_result.should.be.true
+            })).timeout(15000)
+    });
+});
\ No newline at end of file