-
Notifications
You must be signed in to change notification settings - Fork 86
Description
I tested both on Chrome under Arch Linux
Press ctrl + shift + i
If not in the console already press console at the top, type allow pasting, then paste one of these commands
You need to start the lesson for the manual one, so press start or resume
For the Auto one, it detects if the start or resume buttons are shown, so all you have to do is run the script
This one is manual, you have to run on each lesson
function findAPI() {
var attempts = 0;
var maxAttempts = 50;
// Check for JKOAPI iframe in current window
var checkAPI = setInterval(function() {
attempts++;
try {
// Try parent window first
if (window.parent && window.parent.JKOAPI &&
window.parent.JKOAPI.document &&
window.parent.JKOAPI.document.API_1484_11) {
clearInterval(checkAPI);
executeCommands(window.parent.JKOAPI.document.API_1484_11);
return;
}
// Try top window
if (window.top && window.top.JKOAPI &&
window.top.JKOAPI.document &&
window.top.JKOAPI.document.API_1484_11) {
clearInterval(checkAPI);
executeCommands(window.top.JKOAPI.document.API_1484_11);
return;
}
// Try current window
if (window.JKOAPI && window.JKOAPI.document &&
window.JKOAPI.document.API_1484_11) {
clearInterval(checkAPI);
executeCommands(window.JKOAPI.document.API_1484_11);
return;
}
if (attempts >= maxAttempts) {
clearInterval(checkAPI);
console.error("SCORM API not found after 5 seconds");
}
} catch (e) {
console.log("Attempt " + attempts + " - Error:", e.message);
if (attempts >= maxAttempts) {
clearInterval(checkAPI);
console.error("Failed to access API:", e);
}
}
}, 100);
}
function executeCommands(API) {
if (!API) {
console.error("API is null");
return;
}
console.log("✓ SCORM API found!");
try {
var result = API.SetValue('cmi.completion_status', 'completed');
console.log("✓ Completion status set, result:", result);
// Try to find and submit the form
var courseHeader = null;
if (window.parent) {
courseHeader = window.parent.document.getElementsByName("courseheader")[0];
}
if (!courseHeader && window.top) {
courseHeader = window.top.document.getElementsByName("courseheader")[0];
}
if (courseHeader && courseHeader.contentDocument) {
var form = courseHeader.contentDocument.getElementById("c");
if (form) {
form.submit();
console.log("✓ Form submitted");
} else {
console.log("⚠ Form 'c' not found in courseheader");
}
} else {
console.log("⚠ courseheader iframe not accessible");
}
} catch (e) {
console.error("✗ Error executing commands:", e);
}
}
findAPI();
This one is automated, it ends the course at 93 or more completion as that seems to be where it stops for me, if its not for you, you can run (run this while the script is running)
JKOAuto.setThreshold(100) // Set to 100%
or change the var config at the top
var config = {
progressThreshold: 100, // Set to 100%
checkInterval: 2000,
apiWaitTime: 100,
maxApiAttempts: 50,
maxRetries: 5 // Max times to retry a lesson before moving on
};
The script has wait logic built-in in case the lesson is over 10 hrs so it reduces the chance of getting flagged.
As the script is now, it marks tests as complete but you don't get a score so the lesson gets marked as invalid, I will be looking into a way to inject a score for tests.
// ========================================
// JKO Course Automation Script - With Duration-Based Delay
// ========================================
(function() {
'use strict';
var config = {
progressThreshold: 93,
checkInterval: 2000,
apiWaitTime: 100,
maxApiAttempts: 50,
maxRetries: 5,
hourThreshold: 8 // Wait if course is > 8 hours
};
var state = {
isRunning: false,
currentProgress: 0,
hasStarted: false,
currentLessonRetries: 0,
lastLessonId: null,
waitTimeRemaining: 0,
isWaiting: false
};
function log(message, type) {
var prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : '🔄';
console.log(prefix + ' [JKO Auto] ' + message);
}
// ========================================
// Extract course duration from title
// ========================================
function getCourseDuration() {
try {
var windows = [window, window.parent, window.top];
for (var w = 0; w < windows.length; w++) {
try {
var win = windows[w];
if (!win || !win.document) continue;
var titleEl = win.document.getElementById("playerCourseTitle");
if (titleEl) {
var titleText = titleEl.textContent || titleEl.innerText;
// Look for pattern like "(20 hrs)" or "(20hrs)" or "(20 hr)"
var match = titleText.match(/\((\d+)\s*hrs?\)/i);
if (match) {
var hours = parseInt(match[1]);
log("Course duration detected: " + hours + " hours", 'info');
return hours;
}
}
} catch (e) {
// Try next window
}
}
log("No course duration found in title", 'info');
return 0;
} catch (e) {
log("Error detecting course duration: " + e.message, 'error');
return 0;
}
}
// ========================================
// Calculate and apply wait time
// ========================================
function calculateWaitTime() {
var hours = getCourseDuration();
if (hours > config.hourThreshold) {
var waitMinutes = hours;
var waitMs = waitMinutes * 60 * 1000;
log("Course is " + hours + " hours (>" + config.hourThreshold + " hrs threshold)", 'info');
log("⏳ Waiting " + waitMinutes + " minutes before starting automation...", 'success');
state.isWaiting = true;
state.waitTimeRemaining = waitMinutes;
// Update countdown every minute
var countdownInterval = setInterval(function() {
state.waitTimeRemaining--;
if (state.waitTimeRemaining > 0) {
log("⏳ Waiting... " + state.waitTimeRemaining + " minutes remaining", 'info');
} else {
clearInterval(countdownInterval);
state.isWaiting = false;
log("✅ Wait complete! Starting automation now", 'success');
}
}, 60000); // Every minute
return waitMs;
} else {
log("Course is " + hours + " hours (<=" + config.hourThreshold + " hrs) - no wait needed", 'info');
return 0;
}
}
// ========================================
// Get iframe by name from any window level
// ========================================
function getIframe(name) {
var windows = [window, window.parent, window.top];
for (var w = 0; w < windows.length; w++) {
try {
var win = windows[w];
if (!win || !win.document) continue;
var iframes = win.document.getElementsByName(name);
if (iframes.length > 0) {
return iframes[0];
}
} catch (e) {
// Ignore cross-origin errors
}
}
return null;
}
// ========================================
// Check if element is visible
// ========================================
function isElementVisible(element) {
if (!element) return false;
var style = element.style;
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
try {
var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
return false;
}
} catch (e) {
// Continue if we can't get computed style
}
if (element.offsetWidth === 0 || element.offsetHeight === 0) {
return false;
}
return true;
}
// ========================================
// Check if current lesson is completed (has checkmark)
// ========================================
function isCurrentLessonCompleted() {
try {
var iframe = getIframe("coursegenerate");
if (!iframe || !iframe.contentDocument) {
return null;
}
var selectedItems = iframe.contentDocument.querySelectorAll('.menuTabItemSelected');
for (var i = 0; i < selectedItems.length; i++) {
var selectedItem = selectedItems[i];
var menuTabItem = selectedItem.closest('.menuTabItem');
var lessonId = menuTabItem ? menuTabItem.id : null;
if (lessonId && lessonId !== state.lastLessonId) {
state.lastLessonId = lessonId;
state.currentLessonRetries = 0;
log("New lesson detected: " + lessonId, 'info');
}
var iconContainer = menuTabItem ?
menuTabItem.querySelector('.menuTabItemIconContainer') :
selectedItem.parentElement.querySelector('.menuTabItemIconContainer');
if (iconContainer) {
var classes = iconContainer.className;
if (classes.indexOf('menuTabLessonIcon_completed') !== -1) {
return true;
} else {
return false;
}
}
}
return null;
} catch (e) {
log("Error checking lesson completion: " + e.message, 'error');
return null;
}
}
// ========================================
// Retry current lesson
// ========================================
function retryCurrentLesson() {
try {
var iframe = getIframe("coursegenerate");
if (!iframe || !iframe.contentDocument) {
return false;
}
var selectedItems = iframe.contentDocument.querySelectorAll('.menuTabItemSelected a');
if (selectedItems.length > 0) {
var lessonLink = selectedItems[0];
log("Retrying current lesson...", 'info');
lessonLink.click();
return true;
}
} catch (e) {
log("Error retrying lesson: " + e.message, 'error');
}
return false;
}
// ========================================
// Start/Resume Button Detection
// ========================================
function checkAndClickStartResume() {
try {
var iframe = getIframe("courseheader");
if (!iframe || !iframe.contentDocument) {
return false;
}
var resumeButton = iframe.contentDocument.getElementById("two");
if (resumeButton && isElementVisible(resumeButton)) {
log("Found visible Resume button - clicking it!", 'success');
resumeButton.click();
state.hasStarted = true;
return true;
}
var startButton = iframe.contentDocument.getElementById("one");
if (startButton && isElementVisible(startButton)) {
log("Found visible Start button - clicking it!", 'success');
startButton.click();
state.hasStarted = true;
return true;
}
return false;
} catch (e) {
log("Error checking Start/Resume: " + e.message, 'error');
return false;
}
}
// ========================================
// Progress Detection
// ========================================
function getProgress() {
var iframeNames = ['courseheader', 'coursegenerate', 'text'];
var windows = [window, window.parent, window.top];
for (var w = 0; w < windows.length; w++) {
try {
var win = windows[w];
if (!win || !win.document) continue;
var progressEl = win.document.getElementById("lp");
if (progressEl) {
var progress = parseInt(progressEl.textContent || progressEl.innerText);
if (!isNaN(progress)) {
return progress;
}
}
for (var i = 0; i < iframeNames.length; i++) {
try {
var iframes = win.document.getElementsByName(iframeNames[i]);
for (var j = 0; j < iframes.length; j++) {
var iframe = iframes[j];
if (iframe && iframe.contentDocument) {
progressEl = iframe.contentDocument.getElementById("lp");
if (progressEl) {
progress = parseInt(progressEl.textContent || progressEl.innerText);
if (!isNaN(progress)) {
return progress;
}
}
}
}
} catch (e) {
// Cross-origin error
}
}
} catch (e) {
// Window access error
}
}
return 0;
}
// ========================================
// Button Click Functions
// ========================================
function clickNextLesson() {
try {
var iframe = getIframe("courseheader");
if (iframe && iframe.contentDocument) {
var nextButton = iframe.contentDocument.getElementById("four");
if (nextButton && isElementVisible(nextButton)) {
log("Clicking Next Lesson button...", 'info');
nextButton.click();
return true;
}
}
} catch (e) {
log("Error clicking Next Lesson: " + e.message, 'error');
}
return false;
}
function clickExitCourse() {
try {
var iframe = getIframe("courseheader");
if (iframe && iframe.contentDocument) {
var exitButtons = iframe.contentDocument.querySelectorAll('a.button[href="javascript:close();"]');
for (var i = 0; i < exitButtons.length; i++) {
if (exitButtons[i].textContent.indexOf("Exit") !== -1) {
log("🎉 Progress reached " + state.currentProgress + "% - Clicking Exit Course!", 'success');
exitButtons[i].click();
return true;
}
}
}
} catch (e) {
log("Error clicking Exit: " + e.message, 'error');
}
return false;
}
// ========================================
// SCORM API Functions
// ========================================
function findAPI() {
try {
if (window.parent && window.parent.JKOAPI &&
window.parent.JKOAPI.document &&
window.parent.JKOAPI.document.API_1484_11) {
return window.parent.JKOAPI.document.API_1484_11;
}
if (window.top && window.top.JKOAPI &&
window.top.JKOAPI.document &&
window.top.JKOAPI.document.API_1484_11) {
return window.top.JKOAPI.document.API_1484_11;
}
if (window.JKOAPI && window.JKOAPI.document &&
window.JKOAPI.document.API_1484_11) {
return window.JKOAPI.document.API_1484_11;
}
} catch (e) {
log("Error finding API: " + e.message, 'error');
}
return null;
}
function completeCurrentLesson() {
var API = findAPI();
if (API) {
try {
API.SetValue('cmi.completion_status', 'completed');
return true;
} catch (e) {
log("Error setting completion: " + e.message, 'error');
}
}
return false;
}
// ========================================
// Main Automation Loop
// ========================================
function automationLoop() {
if (!state.isRunning) return;
// Check for Start/Resume button if we haven't clicked it yet
if (!state.hasStarted) {
if (checkAndClickStartResume()) {
setTimeout(automationLoop, 3000);
return;
}
}
var currentProgress = getProgress();
if (currentProgress !== state.currentProgress) {
state.currentProgress = currentProgress;
log("Progress: " + currentProgress + "%", 'success');
}
// Check if we've reached the threshold
if (currentProgress >= config.progressThreshold) {
log("🎉 Target progress reached (" + currentProgress + "% >= " + config.progressThreshold + "%)!", 'success');
state.isRunning = false;
setTimeout(function() {
clickExitCourse();
}, 1000);
return;
}
// Complete the lesson via SCORM
completeCurrentLesson();
// Wait a moment for SCORM to register
setTimeout(function() {
// Check if current lesson is completed (has checkmark)
var isCompleted = isCurrentLessonCompleted();
if (isCompleted === true) {
// Lesson is completed, move to next
log("Lesson verified as complete - moving to next", 'success');
state.currentLessonRetries = 0;
setTimeout(function() {
clickNextLesson();
}, 500);
} else if (isCompleted === false) {
// Lesson is NOT completed
state.currentLessonRetries++;
if (state.currentLessonRetries >= config.maxRetries) {
log("Max retries reached (" + config.maxRetries + ") - forcing next lesson", 'error');
state.currentLessonRetries = 0;
setTimeout(function() {
clickNextLesson();
}, 500);
} else {
log("Lesson not complete - retry " + state.currentLessonRetries + "/" + config.maxRetries, 'info');
setTimeout(function() {
retryCurrentLesson();
}, 1000);
}
} else {
// Unknown status - proceed with caution
log("Cannot verify completion - attempting next anyway", 'error');
setTimeout(function() {
clickNextLesson();
}, 500);
}
// Schedule next iteration
setTimeout(automationLoop, config.checkInterval);
}, 1000);
}
// ========================================
// Initialization
// ========================================
function startAutomation() {
var attempts = 0;
var checkAPI = setInterval(function() {
attempts++;
var API = findAPI();
if (API) {
clearInterval(checkAPI);
log("SCORM API found!", 'success');
log("Will auto-exit at " + config.progressThreshold + "% or higher", 'info');
// Check course duration and calculate wait time
var waitTime = calculateWaitTime();
if (waitTime > 0) {
// Wait before starting
setTimeout(function() {
log("Wait period complete - starting automation", 'success');
checkAndClickStartResume();
state.isRunning = true;
setTimeout(automationLoop, 2000);
}, waitTime);
} else {
// Start immediately
checkAndClickStartResume();
state.isRunning = true;
setTimeout(automationLoop, 2000);
}
} else if (attempts >= config.maxApiAttempts) {
clearInterval(checkAPI);
log("Starting without API", 'error');
// Check wait time even without API
var waitTime = calculateWaitTime();
if (waitTime > 0) {
setTimeout(function() {
checkAndClickStartResume();
state.isRunning = true;
setTimeout(automationLoop, 2000);
}, waitTime);
} else {
checkAndClickStartResume();
state.isRunning = true;
setTimeout(automationLoop, 2000);
}
}
}, config.apiWaitTime);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startAutomation);
} else {
startAutomation();
}
// ========================================
// Manual Controls
// ========================================
window.JKOAuto = {
start: function() {
if (!state.isRunning) {
log("Manually starting automation", 'info');
state.isRunning = true;
automationLoop();
}
},
stop: function() {
log("Stopping automation", 'info');
state.isRunning = false;
},
status: function() {
return {
running: state.isRunning,
progress: state.currentProgress,
threshold: config.progressThreshold,
hasStarted: state.hasStarted,
currentLessonRetries: state.currentLessonRetries,
lastLessonId: state.lastLessonId,
isWaiting: state.isWaiting,
waitTimeRemaining: state.waitTimeRemaining
};
},
setThreshold: function(value) {
config.progressThreshold = value;
log("Progress threshold set to " + value + "%", 'info');
},
setMaxRetries: function(value) {
config.maxRetries = value;
log("Max retries set to " + value, 'info');
},
setHourThreshold: function(value) {
config.hourThreshold = value;
log("Hour threshold set to " + value + " hours", 'info');
},
getDuration: getCourseDuration,
clickNext: clickNextLesson,
clickExit: clickExitCourse,
clickStartResume: checkAndClickStartResume,
retryLesson: retryCurrentLesson,
checkCompletion: isCurrentLessonCompleted,
getProgress: getProgress,
testProgress: function() {
var progress = getProgress();
console.log("Current progress: " + progress + "%");
return progress;
}
};
log("Automation script loaded. Use window.JKOAuto for manual control", 'success');
})();
More commands while the auto script is running
// Check what's happening
JKOAuto.status()
// Check current progress
JKOAuto.testProgress()
// Change threshold to 95%
JKOAuto.setThreshold(95)
// Stop automation
JKOAuto.stop()
// Start it again
JKOAuto.start()
// Manually click next
JKOAuto.clickNext()
// Change retry limit
JKOAuto.setMaxRetries(5)
// Change threshold (default: 8)
JKOAuto.setHourThreshold(10)
// Check detected course hours
JKOAuto.getDuration()
// Shows isWaiting and waitTimeRemaining
JKOAuto.status()