Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tests/e2e/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
7 changes: 5 additions & 2 deletions tests/e2e/runners/chrome/chromium.e2e.runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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);
4 changes: 4 additions & 0 deletions tests/e2e/runners/edge/edge18.bs.runner.js
Original file line number Diff line number Diff line change
@@ -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 */

Expand Down Expand Up @@ -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);
2 changes: 2 additions & 0 deletions tests/e2e/runners/edge/ieMode.e2e.runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -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']);
7 changes: 5 additions & 2 deletions tests/e2e/runners/edge/microsoftEdge.e2e.runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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);
7 changes: 5 additions & 2 deletions tests/e2e/runners/firefox/firefox.e2e.runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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);
4 changes: 4 additions & 0 deletions tests/e2e/runners/safari/safari14.bs.runner.js
Original file line number Diff line number Diff line change
@@ -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 */

Expand Down Expand Up @@ -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']);
230 changes: 230 additions & 0 deletions tests/e2e/spec/tonedear.e2e.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/**
* tonedear.e2e.spec.js : End-to-end tests
*/
import { By, until } from 'selenium-webdriver';
import assert from 'assert';
import paths from '../paths.js';
import fs from 'fs'

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

THEBOSS0369 marked this conversation as resolved.
Show resolved Hide resolved
it('Should navigate from main page to Android & iOS section', async function () {
// 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"]'));

// Verify that the element is found
// console.log('Android & iOS App link found:', androidLink !== null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to uncomment this, to check if the link is found.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done Sir


// Scroll the element into view and click it
await driver.executeScript('arguments[0].scrollIntoView(true);', androidLink);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the element needs to be scrolled into view. You could try without this (comment it out).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done Sir

await driver.wait(until.elementIsVisible(androidLink), 10000); // Wait until it's visible
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you've already found the element (androidLink), it doesn't seem necessary to wait for it to be visible, as you wouldn't have found it if it weren't already visible. You could also try commenting this out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done Sir

await androidLink.click();

// Take a screenshot after clicking for debugging
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem with this is that it's impossible to see screenshots in GitHub Actions AFAIK!

await driver.takeScreenshot().then((image) => {
fs.writeFileSync('postClickScreenshot.png', image, 'base64');
});

// 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
};
Binary file added tests/zims/tonedear/tonedear.com_en_2024-09.zim
Binary file not shown.
Loading