Added an automated Selenium UI test for a small Zimit2 archive (#1286)

* Selenium ui test

* Added android test

* New test

* Fixed all the tests

* Fixed the failing tests

* Update tonedear.e2e.spec.js

* Reducing Time Wait

* Adding tests in every files

* Fixing tests again

* Fixing tests again 2

* Increased Time Out for images verififcation

* Fixing tests

* Update tonedear.e2e.spec.js

* Removing of Dialogue box which fails the tests & increasing tests on different browsers

* Delete tests/e2e/spec/tonedear.js

* Remove unwanted image files

* Removing extra test made on new versions of browsers

* removing reusing same driver in test file ff70

* Fixing the test fail issue

* increasing the timeout and remove the tests from ff70 to test

* fixing tests

* fixing tests

* Testing if bs works or not

* trying again

* Update edge18.bs.runner.js

* Update firefox70.bs.runner.js

* Update firefox70.bs.runner.js

* Adding all the working code from Dummy PR

* Removed Unnecessary codes from tonedear.e2e.spec.js

* fixed service worker const issue

* Added service worker api testing

Signed-off-by: THEBOSS0369 <anujkumsharma9876@gmail.com>

---------

Signed-off-by: THEBOSS0369 <anujkumsharma9876@gmail.com>
This commit is contained in:
Anuj Kumar Sharma 2025-01-27 09:42:28 +05:30 committed by GitHub
parent 56b2a5c671
commit 47db0e7efd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 300 additions and 20 deletions

View File

@ -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
};

View File

@ -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());

View File

@ -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);

View File

@ -1,11 +1,13 @@
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 */
// Input capabilities
const capabilities = {
'browserstack.idleTimeout': 300,
'bstack:options': {
os: 'Windows',
osVersion: '10',
@ -35,8 +37,10 @@ async function loadEdgeLegacyDriver () {
return driver;
};
const driver_edge_legacy = await loadEdgeLegacyDriver();
await legacyRayCharles.runTests(driver_edge_legacy);
// For this runner, we must use a single driver for all tests to avoid the other drivers
// timing out while earlier tests complete
const singleDriver = await loadEdgeLegacyDriver();
const driver_edge_gutenberg = await loadEdgeLegacyDriver();
await gutenbergRo.runTests(driver_edge_gutenberg);
await legacyRayCharles.runTests(singleDriver, null, true);
await gutenbergRo.runTests(singleDriver, null, true);
await tonedear.runTests(singleDriver);

View File

@ -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']);

View File

@ -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);

View File

@ -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);

View File

@ -1,5 +1,6 @@
import { Builder } from 'selenium-webdriver';
import gutenbergRo from '../../spec/gutenberg_ro.e2e.spec.js';
import tonedear from '../../spec/tonedear.e2e.spec.js';
/* eslint-disable camelcase */
// Input capabilities
@ -31,9 +32,13 @@ async function loadFirefoxDriver () {
};
const driver_gutenberg_fx = await loadFirefoxDriver();
const driver_tonedear_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 tests in ServiceWorker mode and Tonedear tests in JQuery only for this browser version');
console.log(' ');
await gutenbergRo.runTests(driver_gutenberg_fx, ['serviceworker']);
await gutenbergRo.runTests(driver_gutenberg_fx);
// Skipping Tonedear tests in SW mode for Firefox 70 due to unsupported navigation issues
// Reason-> Because the browsers below Firefox 77 does not support the replaceAll method, which is used in the Zimit
await tonedear.runTests(driver_tonedear_fx, ['jquery']);

View File

@ -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']);

View File

@ -41,9 +41,10 @@ const gutenbergRoBaseFile = BROWSERSTACK ? '/tests/zims/gutenberg-ro/gutenberg_r
* Run the tests
* @param {WebDriver} driver Selenium WebDriver object
* @param {Array} modes Array of modes to run the tests in
* @param {boolean} keepDriver Whether to keep the driver open after the tests have run
* @returns {Promise<void>} A Promise for the completion of the tests
*/
function runTests (driver, modes) {
function runTests (driver, modes, keepDriver) {
let browserName, browserVersion;
driver.getCapabilities().then(function (caps) {
browserName = caps.get('browserName');
@ -180,7 +181,7 @@ function runTests (driver, modes) {
} else {
// Skip remaining SW mode tests if the browser does not support the SW API
console.log('\x1b[33m%s\x1b[0m', ' Skipping SW mode tests because browser does not support API');
await driver.quit();
if (!keepDriver) await driver.quit();
}
// Disable source verification in SW mode as the dialogue box gave incosistent test results in automated tests
if (mode === 'serviceworker') {
@ -397,8 +398,8 @@ function runTests (driver, modes) {
assert.ok(downloadFileStatus);
// exit if every test and mode is completed
if (mode === modes[modes.length - 1]) {
return driver.quit();
if (mode === modes[modes.length - 1] && !keepDriver) {
await driver.quit();
}
});
});

View File

@ -58,9 +58,10 @@ console.log('\nLoading archive:\n' + rayCharlesAllParts + '\n');
* Run the tests
* @param {WebDriver} driver Selenium WebDriver object
* @param {array} modes Array of modes to run the tests in
* @param {boolean} keepDriver Whether to keep the driver open after the tests have run
* @returns {Promise<void>} A Promise for the completion of the tests
*/
function runTests (driver, modes) {
function runTests (driver, modes, keepDriver) {
let browserName, browserVersion;
driver.getCapabilities().then(function (caps) {
browserName = caps.get('browserName');
@ -192,7 +193,7 @@ function runTests (driver, modes) {
} else {
// Skip remaining SW mode tests if the browser does not support the SW API
console.log('\x1b[33m%s\x1b[0m', ' Skipping SW mode tests because browser does not support API');
await driver.quit();
if (!keepDriver) await driver.quit();
}
// Disable source verification in SW mode as the dialogue box gave incosistent test results in automated tests
if (mode === 'serviceworker') {
@ -345,7 +346,7 @@ function runTests (driver, modes) {
const title = await driver.findElement(By.id('titleHeading')).getText();
assert.equal('Ray Charles', title);
// If we have reached the last mode, quit the driver
if (mode === modes[modes.length - 1]) {
if (mode === modes[modes.length - 1] && !keepDriver) {
await driver.quit();
}
});

View File

@ -0,0 +1,248 @@
/* eslint-disable no-undef */
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';
const tonedearBaseFile = BROWSERSTACK ? '/tests/zims/tonedear/tonedear.com_en_2024-09.zim' : paths.tonedearBaseFile;
/**
* Run the tests
* @param {WebDriver} driver Selenium WebDriver object
* @param {Array} modes Array of modes to run the tests in
* @param {boolean} keepDriver Whether to keep the driver open after the tests have run
* @returns {Promise<void>} A Promise for the completion of the tests
*/
function runTests (driver, modes, keepDriver) {
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 the implicit wait to 3 seconds
driver.manage().setTimeouts({ implicit: 3000 });
// Run in both jquery and serviceworker modes by default
if (!modes) {
modes = ['jquery', 'serviceworker'];
}
modes.forEach(function (mode) {
let serviceWorkerAPI = true;
describe('Tonedear test ' + (mode === 'jquery' ? '[JQuery mode]' : '[SW mode]'), function () {
this.timeout(60000);
this.slow(10000);
it('Load Kiwix JS and check 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.wait(async function () {
const elementIsVisible = await driver.executeScript(
'var el=arguments[0]; el.scrollIntoView(true); setTimeout(function () {el.click();}, 50); return el.offsetParent;',
modeSelector
);
return elementIsVisible;
}, 5000);
await driver.sleep(1300);
// Check for and click any approve button in dialogue box
try {
const activeAlertModal = await driver.findElement(By.css('.modal[style*="display: block"]'));
if (activeAlertModal) {
// Check if ServiceWorker mode API is supported
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 === 'jquery' || serviceWorkerAPI) {
// Wait until the mode has switched
await driver.sleep(2000);
let serviceWorkerStatus = await driver.findElement(By.id('serviceWorkerStatus')).getText();
try {
if (mode === 'serviceworker') {
assert.ok(true, /and\sregistered/i.test(serviceWorkerStatus));
} else {
assert.ok(true, /not\sregistered|unavailable/i.test(serviceWorkerStatus));
}
} catch (e) {
if (!~modes.indexOf('serviceworker')) {
// We can't switch to serviceworker mode if it is not being tested, so we should fail the test
throw e;
}
// We failed to switch modes, so let's try switching back and switching to this mode again
console.log('\x1b[33m%s\x1b[0m', ' Failed to switch to ' + mode + ' mode, trying again...');
let otherModeSelector;
await driver.wait(async function () {
otherModeSelector = await driver.findElement(By.id(mode === 'jquery' ? 'serviceworkerModeRadio' : 'jqueryModeRadio'));
}, 5000);
// Click the other mode selector
await otherModeSelector.click();
// Wait until the mode has switched
await driver.sleep(330);
// Click the mode selector again
await modeSelector.click();
// Wait until the mode has switched
await driver.sleep(330);
serviceWorkerStatus = await driver.findElement(By.id('serviceWorkerStatus')).getText();
if (mode === 'serviceworker') {
assert.equal(true, /and\sregistered/i.test(serviceWorkerStatus));
} else {
assert.equal(true, /not\sregistered|unavailable/i.test(serviceWorkerStatus));
}
}
} else {
// Skip remaining SW mode tests if the browser does not support the SW API
console.log('\x1b[33m%s\x1b[0m', ' Skipping SW mode tests because browser does not support API');
if (!keepDriver) await driver.quit();
return;
}
// Disable source verification in SW mode as the dialogue box gave incosistent test results in automated tests
if (mode === 'serviceworker') {
const sourceVerificationCheckbox = await driver.findElement(By.id('enableSourceVerification'));
if (await sourceVerificationCheckbox.isSelected()) {
await sourceVerificationCheckbox.click();
}
}
});
it('Load Tonedear archive', async function () {
if (!serviceWorkerAPI && mode === 'serviceworker') {
console.log('\x1b[33m%s\x1b[0m', ' - Following test skipped:');
return;
}
if (!BROWSERSTACK) {
const archiveFiles = await driver.findElement(By.id('archiveFiles'));
await archiveFiles.sendKeys(tonedearBaseFile);
await driver.executeScript('window.setLocalArchiveFromFileSelect();');
const filesLength = await driver.executeScript('return document.getElementById("archiveFiles").files.length');
assert.equal(1, filesLength);
} else {
await driver.executeScript('var files = arguments[0]; window.setRemoteArchives.apply(this, files);', [tonedearBaseFile]);
await driver.sleep(1300);
}
});
it('Navigate to Android & iOS section', async function () {
if (!serviceWorkerAPI && mode === 'serviceworker') {
console.log('\x1b[33m%s\x1b[0m', ' - Following test skipped:');
return;
}
await driver.sleep(2000); // Give time for content to load
await driver.switchTo().frame('articleContent');
const androidIosLink = await driver.wait(until.elementLocated(By.css('a[href="android-ios-ear-training-app"]')), 5000);
await androidIosLink.click();
// Switch back to default content before handling dialogs or verifying content
await driver.switchTo().defaultContent();
// Wait time
await driver.sleep(1000);
});
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);
}, 10000, 'No visible store images found after 10 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;
}
}
// exit if every test and mode is completed
if (mode === modes[modes.length - 1] && !keepDriver) {
await driver.quit();
}
});
});
});
}
export default {
runTests: runTests
};

Binary file not shown.

View File

@ -1864,6 +1864,8 @@ async function archiveReadyCallback (archive) {
if (settingsStore.getItem('trustedZimFiles') === null) {
settingsStore.setItem('trustedZimFiles', '', Infinity);
}
// This is used for testing: if the noPrompts flag is set, we skip the source verification
if (params.noPrompts) params.sourceVerification = false;
if (params.sourceVerification && (params.contentInjectionMode === 'serviceworker' || params.contentInjectionMode === 'serviceworkerlocal')) {
// Check if source of the zim file can be trusted.
if (!(settingsStore.getItem('trustedZimFiles').includes(archive.file.name))) {