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 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6859ab9
Selenium ui test
THEBOSS0369 Nov 11, 2024
f1c5ec1
Added android test
THEBOSS0369 Nov 15, 2024
66aeef5
New test
THEBOSS0369 Nov 17, 2024
badde6f
Fixed all the tests
THEBOSS0369 Nov 17, 2024
b8543f0
Merge branch 'main' into selenium-test
THEBOSS0369 Nov 17, 2024
f0b17ae
Fixed the failing tests
THEBOSS0369 Nov 18, 2024
3cfc527
Merge remote-tracking branch 'origin/selenium-test' into selenium-test
THEBOSS0369 Nov 18, 2024
92727c3
Update tonedear.e2e.spec.js
THEBOSS0369 Nov 18, 2024
f7916ca
Reducing Time Wait
THEBOSS0369 Nov 18, 2024
ec085d0
Adding tests in every files
THEBOSS0369 Nov 18, 2024
cb7f22b
Merge remote-tracking branch 'origin/selenium-test' into selenium-test
THEBOSS0369 Nov 18, 2024
1bc38fd
Fixing tests again
THEBOSS0369 Nov 18, 2024
d381a37
Fixing tests again 2
THEBOSS0369 Nov 18, 2024
db01d15
Increased Time Out for images verififcation
THEBOSS0369 Nov 19, 2024
5a6b02f
Fixing tests
THEBOSS0369 Nov 19, 2024
f349db4
Merge remote-tracking branch 'origin/selenium-test' into selenium-test
THEBOSS0369 Nov 19, 2024
f2ebf5f
Update tonedear.e2e.spec.js
THEBOSS0369 Nov 19, 2024
cb14eef
Removing of Dialogue box which fails the tests & increasing tests on …
THEBOSS0369 Dec 2, 2024
1e5e6dd
Merge remote-tracking branch 'origin/selenium-test' into selenium-test
THEBOSS0369 Dec 2, 2024
5a53ffe
Delete tests/e2e/spec/tonedear.js
THEBOSS0369 Dec 2, 2024
06c33dc
Remove unwanted image files
THEBOSS0369 Dec 2, 2024
088e354
Merge remote-tracking branch 'origin/selenium-test' into selenium-test
THEBOSS0369 Dec 2, 2024
1ce071d
Removing extra test made on new versions of browsers
THEBOSS0369 Dec 4, 2024
125bbc1
Merge branch 'main' into selenium-test
THEBOSS0369 Dec 4, 2024
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 () {
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be better just to say "Navigate from ...." (without "Should").

// 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.

Rather than using console.log here, make this into a test and assert that androidLink !== null. If it is null, then the test would fail. The formatting would be better in the log too.


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

// 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!

Copy link
Member

Choose a reason for hiding this comment

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

Since we have videos taken on BrowserStack, I think this screenshotting probably isn't necessary. Best to remove it.

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