diff --git a/tests/e2e/paths.js b/tests/e2e/paths.js index 6ba8e0e40..bfe6f76b7 100644 --- a/tests/e2e/paths.js +++ b/tests/e2e/paths.js @@ -6,10 +6,12 @@ import path from 'path'; const rayCharlesBaseFile = path.resolve('./tests/zims/legacy-ray-charles/wikipedia_en_ray_charles_2015-06.zimaa'); const gutenbergRoBaseFile = path.resolve('./tests/zims/gutenberg-ro/gutenberg_ro_all_2023-08.zim'); +const tonedearBaseFile = path.resolve('./tests/zims/tonedear/tonedear.com_en_2024-09.zim'); const downloadDir = path.resolve('./tests/'); export default { rayCharlesBaseFile: rayCharlesBaseFile, gutenbergRoBaseFile: gutenbergRoBaseFile, + tonedearBaseFile: tonedearBaseFile, downloadDir: downloadDir }; diff --git a/tests/e2e/runners/chrome/chrome60.bs.runner.js b/tests/e2e/runners/chrome/chrome60.bs.runner.js index 00eafc8e5..6f2fe7766 100644 --- a/tests/e2e/runners/chrome/chrome60.bs.runner.js +++ b/tests/e2e/runners/chrome/chrome60.bs.runner.js @@ -1,6 +1,7 @@ import { Builder } from 'selenium-webdriver'; import { Options } from 'selenium-webdriver/chrome.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedear from '../../spec/tonedear.e2e.spec.js' import paths from '../../paths.js'; /* eslint-disable camelcase */ @@ -37,8 +38,9 @@ async function loadChromeDriver () { // Maximize the window so that full browser state is visible in the screenshots // await driver_chrome.manage().window().maximize(); // Not supported in this version / Selenium -console.log('\x1b[33m%s\x1b[0m', 'Running Gutenberg tests only for this browser version'); +console.log('\x1b[33m%s\x1b[0m', 'Running Gutenberg and Tonedear tests only for this browser version'); console.log(' '); // make sure to use await running tests or we are charged unnecessarily on Browserstack await gutenbergRo.runTests(await loadChromeDriver()); +await tonedear.runTests(await loadChromeDriver()); diff --git a/tests/e2e/runners/chrome/chromium.e2e.runner.js b/tests/e2e/runners/chrome/chromium.e2e.runner.js index 2e3bd7769..ed233e3d8 100644 --- a/tests/e2e/runners/chrome/chromium.e2e.runner.js +++ b/tests/e2e/runners/chrome/chromium.e2e.runner.js @@ -2,6 +2,7 @@ import { Builder } from 'selenium-webdriver'; import { Options } from 'selenium-webdriver/chrome.js'; import legacyRayCharles from '../../spec/legacy-ray_charles.e2e.spec.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedearTests from '../../spec/tonedear.e2e.spec.js'; import paths from '../../paths.js'; /* eslint-disable camelcase */ @@ -20,10 +21,12 @@ async function loadChromiumDriver () { return driver; }; -// Preserve the order of loading, because when a user runs these on local machine, the second driver will be on top of and cover the first one -// so we need to use the second one first +// Preserve the order of loading, because when a user runs these on local machine, the third driver will be on top of and cover the first one +// so we need to use the third one first +const driver_for_tonedear = await loadChromiumDriver(); const driver_for_gutenberg = await loadChromiumDriver(); const driver_for_ray_charles = await loadChromiumDriver(); await legacyRayCharles.runTests(driver_for_ray_charles); await gutenbergRo.runTests(driver_for_gutenberg); +await tonedearTests.runTests(driver_for_tonedear); diff --git a/tests/e2e/runners/edge/edge18.bs.runner.js b/tests/e2e/runners/edge/edge18.bs.runner.js index 47590146d..51b4fd0a2 100644 --- a/tests/e2e/runners/edge/edge18.bs.runner.js +++ b/tests/e2e/runners/edge/edge18.bs.runner.js @@ -1,6 +1,7 @@ import { Builder } from 'selenium-webdriver'; import legacyRayCharles from '../../spec/legacy-ray_charles.e2e.spec.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedear from '../../spec/tonedear.e2e.spec.js'; /* eslint-disable camelcase */ @@ -40,3 +41,6 @@ await legacyRayCharles.runTests(driver_edge_legacy); const driver_edge_gutenberg = await loadEdgeLegacyDriver(); await gutenbergRo.runTests(driver_edge_gutenberg); + +const driver_edge_tonedear = await loadEdgeLegacyDriver(); +await tonedear.runTests(driver_edge_tonedear); diff --git a/tests/e2e/runners/edge/ieMode.e2e.runner.js b/tests/e2e/runners/edge/ieMode.e2e.runner.js index 27b3a4e7e..294ddceae 100644 --- a/tests/e2e/runners/edge/ieMode.e2e.runner.js +++ b/tests/e2e/runners/edge/ieMode.e2e.runner.js @@ -2,6 +2,7 @@ import { Builder } from 'selenium-webdriver'; import { Options } from 'selenium-webdriver/ie.js'; import legacyRayCharles from '../../spec/legacy-ray_charles.e2e.spec.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedear from '../../spec/tonedear.e2e.spec.js'; /* eslint-disable camelcase */ @@ -18,3 +19,4 @@ async function loadIEModeDriver () { await legacyRayCharles.runTests(await loadIEModeDriver(), ['jquery']); await gutenbergRo.runTests(await loadIEModeDriver(), ['jquery']); +await tonedear.runTests(await loadIEModeDriver(), ['jquery']); diff --git a/tests/e2e/runners/edge/microsoftEdge.e2e.runner.js b/tests/e2e/runners/edge/microsoftEdge.e2e.runner.js index 948b27597..a72e388a0 100644 --- a/tests/e2e/runners/edge/microsoftEdge.e2e.runner.js +++ b/tests/e2e/runners/edge/microsoftEdge.e2e.runner.js @@ -2,6 +2,7 @@ import { Builder } from 'selenium-webdriver'; import { Options } from 'selenium-webdriver/edge.js'; import legacyRayCharles from '../../spec/legacy-ray_charles.e2e.spec.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedearTests from '../../spec/tonedear.e2e.spec.js'; /* eslint-disable camelcase */ async function loadMSEdgeDriver () { @@ -17,10 +18,12 @@ async function loadMSEdgeDriver () { return driver; }; -// Preserve the order of loading, because when a user runs these on local machine, the second driver will be on top of and cover the first one -// so we need to use the second one first +// Preserve the order of loading, because when a user runs these on local machine, the third driver will be on top of and cover the first one +// so we need to use the third one first +const driver_for_tonedear = await loadMSEdgeDriver(); const driver_for_gutenberg = await loadMSEdgeDriver(); const driver_for_ray_charles = await loadMSEdgeDriver(); await legacyRayCharles.runTests(driver_for_ray_charles); await gutenbergRo.runTests(driver_for_gutenberg); +await tonedearTests.runTests(driver_for_tonedear); diff --git a/tests/e2e/runners/firefox/firefox.e2e.runner.js b/tests/e2e/runners/firefox/firefox.e2e.runner.js index 28a6e722d..5b830984b 100644 --- a/tests/e2e/runners/firefox/firefox.e2e.runner.js +++ b/tests/e2e/runners/firefox/firefox.e2e.runner.js @@ -2,6 +2,7 @@ import { Builder } from 'selenium-webdriver'; import firefox from 'selenium-webdriver/firefox.js'; import legacyRayCharles from '../../spec/legacy-ray_charles.e2e.spec.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedearTests from '../../spec/tonedear.e2e.spec.js'; import paths from '../../paths.js'; /* eslint-disable camelcase */ @@ -23,10 +24,12 @@ async function loadFirefoxDriver () { return driver; }; -// Preserve the order of loading, because when a user runs these on local machine, the second driver will be on top of and cover the first one -// so we need to use the second one first +// Preserve the order of loading, because when a user runs these on local machine, the third driver will be on top of and cover the first one +// so we need to use the third one first +const driver_for_tonedear = await loadFirefoxDriver(); const driver_for_gutenberg = await loadFirefoxDriver(); const driver_for_ray_charles = await loadFirefoxDriver(); await legacyRayCharles.runTests(driver_for_ray_charles); await gutenbergRo.runTests(driver_for_gutenberg); +await tonedearTests.runTests(driver_for_tonedear); diff --git a/tests/e2e/runners/firefox/firefox70.bs.runner.js b/tests/e2e/runners/firefox/firefox70.bs.runner.js index e500219b2..fd7ab8a2a 100644 --- a/tests/e2e/runners/firefox/firefox70.bs.runner.js +++ b/tests/e2e/runners/firefox/firefox70.bs.runner.js @@ -33,7 +33,7 @@ async function loadFirefoxDriver () { const driver_gutenberg_fx = await loadFirefoxDriver(); // Run test in SW mode only -console.log('\x1b[33m%s\x1b[0m', 'Running Gutenberg tests in ServiceWorker mode only for this browser version'); +console.log('\x1b[33m%s\x1b[0m', 'Running Gutenberg and Tonedear tests in ServiceWorker mode only for this browser version'); console.log(' '); await gutenbergRo.runTests(driver_gutenberg_fx, ['serviceworker']); diff --git a/tests/e2e/runners/safari/safari14.bs.runner.js b/tests/e2e/runners/safari/safari14.bs.runner.js index e8a9f689d..6415bdd9f 100644 --- a/tests/e2e/runners/safari/safari14.bs.runner.js +++ b/tests/e2e/runners/safari/safari14.bs.runner.js @@ -1,6 +1,7 @@ import { Builder } from 'selenium-webdriver'; import legacyRayCharles from '../../spec/legacy-ray_charles.e2e.spec.js'; import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js'; +import tonedearTests from '../../spec/tonedear.e2e.spec.js'; /* eslint-disable camelcase */ @@ -42,3 +43,6 @@ await legacyRayCharles.runTests(driver_legacy_safari, ['jquery']); const driver_gutenberg_safari = await loadSafariDriver(); await gutenbergRo.runTests(driver_gutenberg_safari, ['jquery']); + +const driver_tonedear_safari = await loadSafariDriver(); +await tonedearTests.runTests(driver_tonedear_safari, ['jquery']); diff --git a/tests/e2e/spec/tonedear.e2e.spec.js b/tests/e2e/spec/tonedear.e2e.spec.js new file mode 100644 index 000000000..3ae78149e --- /dev/null +++ b/tests/e2e/spec/tonedear.e2e.spec.js @@ -0,0 +1,237 @@ +/** + * tonedear.e2e.spec.js : End-to-end tests + */ +import { By, until } from 'selenium-webdriver'; +import assert from 'assert'; +import paths from '../paths.js'; + +const BROWSERSTACK = !!process.env.BROWSERSTACK_LOCAL_IDENTIFIER; +const port = BROWSERSTACK ? '8099' : '8080'; + +// Set the archive to load +let tonedearBaseFile = paths.tonedearBaseFile; +if (BROWSERSTACK) { + tonedearBaseFile = '/tests/zims/tonedear/tonedear.com_en_2024-09.zim'; +} + +/* global describe, it */ +/** + * Run the tests + * @param {WebDriver} driver Selenium WebDriver object + * @param {array} modes Array of modes to run the tests in ['jquery', 'serviceworker'] + */ +function runTests (driver, modes) { + // Set default modes if not provided + if (!modes) { + modes = ['jquery', 'serviceworker']; + } + + let browserName, browserVersion; + driver.getCapabilities().then(function (caps) { + browserName = caps.get('browserName'); + browserVersion = caps.get('browserVersion'); + console.log('\nRunning Tonedear tests on: ' + browserName + ' ' + browserVersion); + }); + + // Set implicit wait timeout + driver.manage().setTimeouts({ implicit: 3000 }); + + modes.forEach(function (mode) { + let serviceWorkerAPI = true; + + // eslint-disable-next-line no-undef + describe('Tonedear Test Suite ' + (mode === 'jquery' ? '[JQuery mode]' : '[SW mode]'), function () { + this.timeout(60000); + this.slow(10000); + + it('Load Kiwix JS and verify title', async function () { + await driver.get('http://localhost:' + port + '/dist/www/index.html?noPrompts=true'); + await driver.sleep(1300); + await driver.navigate().refresh(); + await driver.sleep(800); + const title = await driver.getTitle(); + assert.equal('Kiwix', title); + }); + + it('Switch to ' + mode + ' mode', async function () { + const modeSelector = await driver.wait( + until.elementLocated(By.id(mode + 'ModeRadio')) + ); + await driver.executeScript( + 'var el=arguments[0]; el.scrollIntoView(true); setTimeout(function() {el.click();}, 50); return el.offsetParent;', + modeSelector + ); + await driver.sleep(1300); + + try { + const activeAlertModal = await driver.findElement( + By.css('.modal[style*="display: block"]') + ); + if (activeAlertModal) { + serviceWorkerAPI = await driver.findElement(By.id('modalLabel')) + .getText() + .then(function (alertText) { + return !/ServiceWorker\sAPI\snot\savailable/i.test(alertText); + }); + const approveButton = await driver.wait( + until.elementLocated(By.id('approveConfirm')) + ); + await approveButton.click(); + } + } catch (e) { + // Do nothing + } + + if (mode === 'serviceworker') { + // Disable source verification in SW mode as the dialogue box gave inconsistent test results + const sourceVerificationCheckbox = await driver.findElement(By.id('enableSourceVerification')); + if (sourceVerificationCheckbox.isSelected()) { + await sourceVerificationCheckbox.click(); + } + } + }); + + it('Load Tonedear archive and verify content', async function () { + if (!serviceWorkerAPI) { + console.log('\x1b[33m%s\x1b[0m', ' - Following test skipped:'); + this.skip(); + } + + const archiveFiles = await driver.findElement(By.id('archiveFiles')); + await driver.executeScript('arguments[0].style.display = "block";', archiveFiles); + + if (!BROWSERSTACK) { + await archiveFiles.sendKeys(tonedearBaseFile); + await driver.executeScript('window.setLocalArchiveFromFileSelect();'); + } else { + await driver.executeScript( + 'window.setRemoteArchives.apply(this, [arguments[0]]);', + [tonedearBaseFile] + ); + await driver.sleep(1300); + } + }); + + it('Navigate from main page to Android & iOS section', async function () { + // Check for Dialog Box and click any Approve Button in subsequent dialog box + try { + const activeAlertModal = await driver.findElement(By.css('.modal[style*="display: block"]')); + if (activeAlertModal) { + // console.log('Found active alert modal'); + const approveButton = await driver.findElement(By.id('approveConfirm')); + await approveButton.click(); + } + } catch (e) { + // Do nothing + console.log('Modal not found within the timeout. Continuing test...'); + } + + // Switch to the iframe if the content is inside 'articleContent' + await driver.switchTo().frame('articleContent'); + // console.log('Switched to iframe successfully'); + + // Wait until the link "Android & iOS App" is present in the DOM + await driver.wait(async function () { + const contentAvailable = await driver.executeScript('return document.querySelector(\'a[href="android-ios-ear-training-app"]\') !== null;'); + return contentAvailable; + }, 10000); // Increased to 10 seconds for more loading time + + // Find the "Android & iOS App" link + const androidLink = await driver.findElement(By.css('a[href="android-ios-ear-training-app"]')); + + // Test that the element is found + assert(androidLink !== null, 'Android & iOS App link was not found'); + + // Scroll the element into view and click it + // await driver.executeScript('arguments[0].scrollIntoView(true);', androidLink); + // await driver.wait(until.elementIsVisible(androidLink), 10000); // Wait until it's visible + await androidLink.click(); + + // Switch back to the default content + await driver.switchTo().defaultContent(); + }); + + it('Verify Android and iOS store images in ' + (mode === 'jquery' ? 'Restricted' : 'ServiceWorker') + ' mode', async function () { + if (!serviceWorkerAPI && mode === 'jquery') { + // Restricted mode test for data URIs + const androidImage = await driver.findElement(By.css('img[alt="Get it on Google Play"]')); + const iosImage = await driver.findElement(By.css('img[alt="Get the iOS app"]')); + + // Verify src attribute has changed to a data URI + const androidSrc = await androidImage.getAttribute('src'); + const iosSrc = await iosImage.getAttribute('src'); + + assert.ok(androidSrc.startsWith('data:image/png;base64,'), 'Android image src is a data URI'); + assert.ok(iosSrc.startsWith('data:image/png;base64,'), 'iOS image src is a data URI'); + + // Compare the first 30 characters of data URIs + const androidDataSnippet = androidSrc.substring(22, 52); + const iosDataSnippet = iosSrc.substring(22, 52); + + // Expected snippet for comparison + const expectedAndroidSnippet = 'iVBORw0KGgoAAAANSUhEUg'; + const expectedIosSnippet = 'iVBORw0KGgoAAAANSUhEUg'; + + assert.strictEqual(androidDataSnippet, expectedAndroidSnippet, 'Android image data matches expected'); + assert.strictEqual(iosDataSnippet, expectedIosSnippet, 'iOS image data matches expected'); + } else if (serviceWorkerAPI && mode === 'serviceworker') { + try { + // ServiceWorker mode test for image loading + await driver.sleep(3000); + + const swRegistration = await driver.executeScript('return navigator.serviceWorker.ready'); + assert.ok(swRegistration, 'Service Worker is registered'); + + // console.log('Current URL:', await driver.getCurrentUrl()); + + // Switch to the iframe that contains the Android and iOS images + const iframe = await driver.findElement(By.id('articleContent')); + await driver.switchTo().frame(iframe); + + // Wait for images to be visible on the page inside the iframe + await driver.wait(async function () { + const images = await driver.findElements(By.css('img[alt="Get it on Google Play"], img[alt="Get the iOS app"]')); + if (images.length === 0) return false; + + // Check if all images are visible + const visibility = await Promise.all(images.map(async (img) => { + return await img.isDisplayed(); + })); + return visibility.every((isVisible) => isVisible); + }, 30000, 'No visible store images found after 30 seconds'); + + const androidImage = await driver.findElement(By.css('img[alt="Get it on Google Play"]')); + const iosImage = await driver.findElement(By.css('img[alt="Get the iOS app"]')); + + // Wait for images to load and verify dimensions + await driver.wait(async function () { + const androidLoaded = await driver.executeScript('return arguments[0].complete && arguments[0].naturalWidth > 0 && arguments[0].naturalHeight > 0;', androidImage); + const iosLoaded = await driver.executeScript('return arguments[0].complete && arguments[0].naturalWidth > 0 && arguments[0].naturalHeight > 0;', iosImage); + return androidLoaded && iosLoaded; + }, 5000, 'Images did not load successfully'); + + const androidWidth = await driver.executeScript('return arguments[0].naturalWidth;', androidImage); + const androidHeight = await driver.executeScript('return arguments[0].naturalHeight;', androidImage); + + const iosWidth = await driver.executeScript('return arguments[0].naturalWidth;', iosImage); + const iosHeight = await driver.executeScript('return arguments[0].naturalHeight;', iosImage); + + assert.ok(androidWidth > 0 && androidHeight > 0, 'Android image has valid dimensions'); + assert.ok(iosWidth > 0 && iosHeight > 0, 'iOS image has valid dimensions'); + + // Switch back to the main content after finishing the checks + await driver.switchTo().defaultContent(); + } catch (err) { + // If we still can't find the images, log the page source to help debug + console.error('Failed to find store images:', err.message); + throw err; + } + } + }); + }); + }); +} + +export default { + runTests: runTests +}; diff --git a/tests/zims/tonedear/tonedear.com_en_2024-09.zim b/tests/zims/tonedear/tonedear.com_en_2024-09.zim new file mode 100644 index 000000000..0af2d5ab3 Binary files /dev/null and b/tests/zims/tonedear/tonedear.com_en_2024-09.zim differ