From 8bb514bf314c392bdc1f6e8d064d39c590f05645 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Wed, 13 May 2026 15:54:14 +0100 Subject: [PATCH 01/10] settings WIP(html skeleton) --- icon.svg | 1 + metadata.json | 16 +++++ settings.css | 21 +++++++ settings.html | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++ settings.js | 40 ++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 icon.svg create mode 100644 metadata.json create mode 100644 settings.css create mode 100644 settings.html create mode 100644 settings.js diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..cb1265b --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..e9caa94 --- /dev/null +++ b/metadata.json @@ -0,0 +1,16 @@ +{ + "name": "Advanced Code Test", + "type": "assessment", + "properties": { + "type": "test", + "icon": "./icon.svg", + "defaultHeight": 500, + "gradingControls": { + "points": true, + "allowPartialPoints": true, + "useMaximumScore": true, + "definedNumberOfAttempts": true, + "rationale": true + } + } +} diff --git a/settings.css b/settings.css new file mode 100644 index 0000000..2df5188 --- /dev/null +++ b/settings.css @@ -0,0 +1,21 @@ +.settings-content-row-container { + display: flex; + flex-direction: row; + gap: 10px; +} + +.timeout-container { + flex: 1; +} + +.lang-subtype-java-style-settings-config-container { + flex: 1; +} + +.test-case-controls-container { + align-items: end; +} + +.test-case-add-case-container { + flex: 1; +} diff --git a/settings.html b/settings.html new file mode 100644 index 0000000..d002a5d --- /dev/null +++ b/settings.html @@ -0,0 +1,164 @@ + + + + + Settings + + + + + + + +
+
+ + +
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+ +
+
+
+
+ + diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..b48b02b --- /dev/null +++ b/settings.js @@ -0,0 +1,40 @@ +(function () { + const collectSettings = () => { + const instructions = $('#instructions').val() + const timeout = parseInt($('#timeout').val(), 10); + + return {instructions, timeout}; + } + + const exportSettings = () => { + const data = collectSettings(); + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS_RESPONSE, data); + } + + const applySettings = (settings = {}) => { + $('#instructions').val(settings.instructions || ''); + $('#timeout').val(settings.timeout || ''); + } + + const processMessage = (jsonData) => { + console.log('settings iframe processMessage', jsonData) + try { + const {method, data} = JSON.parse(jsonData); + switch (method) { + case window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS: + exportSettings(); + break; + case window.codioAssessmentsHelper.METHODS.GET_SETTINGS_RESPONSE: + applySettings(data.settings); + break; + } + } catch {} + } + + const onLoad = async () => { + window.codioAssessmentsHelper.registerMessageListener(processMessage) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS) + } + + window.addEventListener('load', onLoad); +})() From 4c8e35ab3f88b40cc87fda50088faf059de800a0 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Thu, 14 May 2026 16:30:22 +0100 Subject: [PATCH 02/10] settings visibility management --- settings.css | 8 -------- settings.html | 38 +++++++++++++++++++------------------- settings.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/settings.css b/settings.css index 2df5188..27da8ee 100644 --- a/settings.css +++ b/settings.css @@ -4,14 +4,6 @@ gap: 10px; } -.timeout-container { - flex: 1; -} - -.lang-subtype-java-style-settings-config-container { - flex: 1; -} - .test-case-controls-container { align-items: end; } diff --git a/settings.html b/settings.html index d002a5d..943ee55 100644 --- a/settings.html +++ b/settings.html @@ -28,40 +28,40 @@ -
+
-
+
-
+
-
-
+
-
-
+
@@ -77,7 +77,7 @@
-
+
@@ -90,32 +90,32 @@
-
+
-
+
-
+
-
+
-
+
- @@ -123,26 +123,26 @@
-
+
-
+
-
+
-
+
- diff --git a/settings.js b/settings.js index b48b02b..613079b 100644 --- a/settings.js +++ b/settings.js @@ -31,9 +31,52 @@ } catch {} } + const onSubtypeChanged = (language, subtype) => { + $('.lang-subtype-settings-container').addClass('hide') + $(`.lang-subtype-${language}-${subtype}-settings`).removeClass('hide') + } + + const onLanguageChanged = (language) => { + $('.language-settings-container').addClass('hide') + $(`.${language}-container`).removeClass('hide') + const subtype = $(`#${language}LangSubtype`).val() + onSubtypeChanged(language, subtype) + } + + const bindEvents = () => { + $('#languageType').on('change', function () { + const languageType = $(this).val(); + onLanguageChanged(languageType); + }) + $('.lang-subtype-select').on('change', function () { + const langSubtype = $(this).val(); + onSubtypeChanged($('#languageType').val(), langSubtype); + }) + + // $("html").on("dragover", function(event) { + // event.preventDefault(); + // event.stopPropagation(); + // console.log('dragover', event); + // }); + // + // $("html").on("dragleave", function(event) { + // event.preventDefault(); + // event.stopPropagation(); + // console.log('dragover', event); + // }); + + $("html").on("drop", function(event) { + event.preventDefault(); + event.stopPropagation(); + console.log('drop', event); + }); + } + const onLoad = async () => { window.codioAssessmentsHelper.registerMessageListener(processMessage) window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS) + + bindEvents() } window.addEventListener('load', onLoad); From 7e0acbb18873a958492fc7eb5c4dd48836f881ab Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 19 May 2026 11:05:02 +0100 Subject: [PATCH 03/10] java grammar added to parse tests --- java-grammar.js | 231 ++++++++++++++++++++++++++++++++++++++++++++++++ settings.html | 8 +- settings.js | 110 +++++++++++++++++++---- 3 files changed, 328 insertions(+), 21 deletions(-) create mode 100644 java-grammar.js diff --git a/java-grammar.js b/java-grammar.js new file mode 100644 index 0000000..f2e69a5 --- /dev/null +++ b/java-grammar.js @@ -0,0 +1,231 @@ +window.codioTestAssessment = window.codioTestAssessment || {} +window.codioTestAssessment.javaGrammar = window.codioTestAssessment.javaGrammar || {} + +window.codioTestAssessment.javaGrammar.SLASH = '/' +window.codioTestAssessment.javaGrammar.BACK_SLASH = '\\' +window.codioTestAssessment.javaGrammar.STAR = '*' +window.codioTestAssessment.javaGrammar.DOUBLE_QUOTE = '"' +window.codioTestAssessment.javaGrammar.NEW_LINE = '\n' +window.codioTestAssessment.javaGrammar.LEFT_BRACE = '{' +window.codioTestAssessment.javaGrammar.RIGHT_BRACE = '}' + +window.codioTestAssessment.javaGrammar.packageReg = new RegExp( + 'package\\s+([a-zA-Z_]{1}[a-zA-Z0-9_]*(\\.[a-zA-Z_]{1}[a-zA-Z0-9_]*)*)\\s*;', + 'm' +) +window.codioTestAssessment.javaGrammar.classNameReg = new RegExp( + '\\s*(public|private)?(static)?\\s*class\\s+(\\w+)' + + '\\s*((extends\\s+\\w+)|(implements\\s+\\w+( ,\\w+)*))?\\s*\\{', + 'm' +) +window.codioTestAssessment.javaGrammar.anonClassNameReg = new RegExp('\\s*new\\s+(\\w+)\\s*\\(\\w*\\)\\s*\\{', 'm') + +function removeCommentsAndStrings(content) { + const {DOUBLE_QUOTE, BACK_SLASH, SLASH, STAR, NEW_LINE} = window.codioTestAssessment.javaGrammar + let updatedContent = '' + let isCommentStarted = false + let isStringStarted = false + let isMultiline = false + for (let i = 0, len = content.length; i < len; i++) { + let current = content[i] + if (current === DOUBLE_QUOTE && !isCommentStarted) { + if (content[i - 1] !== BACK_SLASH) { + isStringStarted = !isStringStarted + if (!isStringStarted) { + current = ' ' + } + } + } + if (!isStringStarted && current === SLASH) { + if (content[i + 1] === SLASH) { + isCommentStarted = true + isMultiline = false + } + if (content[i + 1] === STAR) { + isCommentStarted = true + isMultiline = true + } + } + if (isCommentStarted) { + if (isMultiline) { + if (current === STAR && content[i + 1] === SLASH) { + isCommentStarted = false + current = ' ' + i++ + } + } else { + if (current === NEW_LINE) { + isCommentStarted = false + } + } + } + if (isStringStarted || isCommentStarted) { + if (current !== NEW_LINE) { + current = ' ' + } + } + updatedContent += current + } + return updatedContent +} + +const findClassEnd = (content) => { + const {RIGHT_BRACE, LEFT_BRACE} = window.codioTestAssessment.javaGrammar + + let level = 0 + for (let i = 0, len = content.length; i < len; i++) { + let current = content[i] + if (current === RIGHT_BRACE) { + if (level === 0) { + return i + } else { + level-- + } + } else if (current === LEFT_BRACE) { + level++ + } + } + return content.length +} + +const classesInformation = (data) => { + const {classNameReg, anonClassNameReg} = window.codioTestAssessment.javaGrammar + let allClasses = [] + + let classSearch = data + let classNameMatch = classSearch.match(classNameReg) + let offset = 0 + let start = 0 + let end = 0 + while (classNameMatch) { + start = classNameMatch.index + classNameMatch[0].length + classSearch = classSearch.substring(start, classSearch.length) + end = findClassEnd(classSearch) + allClasses.push({ + name: classNameMatch[3], + start: start + offset, + end: start + end + offset + }) + offset += start + classNameMatch = classSearch.match(classNameReg) + } + + let anonClassSearch = data + let anonClassNameMatch = anonClassSearch.match(anonClassNameReg) + offset = 0 + while (anonClassNameMatch) { + start = anonClassNameMatch.index + anonClassNameMatch[0].length + anonClassSearch = anonClassSearch.substring(start, anonClassSearch.length) + end = findClassEnd(anonClassSearch) + allClasses.push({ + start: start + offset, + end: start + end + offset + }) + offset += start + anonClassNameMatch = anonClassSearch.match(anonClassNameReg) + } + + allClasses = allClasses.sort((a, b) => { + return a.start - b.start + }) + + const classesInfo = {} + const processed = [] + + allClasses.forEach(function (item, pos) { + if (pos === 0) { + classesInfo[item.name] = item + classesInfo[item.name].anon = 1 + processed.push(classesInfo[item.name]) + } else { + let lastNested = null + processed.forEach(function (parentItem) { + if (parentItem.start < item.start && parentItem.end > item.end) { + lastNested = parentItem + } + }) + if (lastNested) { + let name + if (!item.name) { + name = lastNested.name + '$' + lastNested.anon + lastNested.anon++ + } else { + name = lastNested.name + '$' + item.name + } + + classesInfo[name] = item + classesInfo[name].anon = 1 + classesInfo[name].name = name + processed.push(classesInfo[name]) + } + } + }) + + return processed +} + +const getClassInfo = (path, content) => { + const {packageReg} = window.codioTestAssessment.javaGrammar + const pathsPart = path.split('/') + const fileName = pathsPart[pathsPart.length - 1] + + const information = { + packageName: '', + className: '', + fileName: fileName, + path: path + } + + let advancedClassInformation + const withoutComments = removeCommentsAndStrings(content) + + const packageMatch = withoutComments.match(packageReg) + if (packageMatch) { + information.packageName = packageMatch[1] + } + advancedClassInformation = classesInformation(withoutComments) + + const lastNested = advancedClassInformation[0] + + if (lastNested) { + information.className = lastNested.name + } + + return information +} + +window.codioTestAssessment.javaGrammar.getJavaInfo = (path, content) => { + const classInformation = getClassInfo(path, content) + if (!classInformation.className) { + const message = 'Java class not found in ' + path + throw new Error(message) + } + const hasTestAnnotations = + content.includes('@Test') || + content.includes('@ParameterizedTest') || + content.includes('@org.junit.jupiter.api.Test') || + content.includes('@org.junit.jupiter.params.ParameterizedTest') + if (!hasTestAnnotations && !content.includes('extends')) { + const message = 'JUnit test not found in ' + path + throw new Error(message) + } + const result = {} + const jUnit5Case = content.includes('.jupiter.') + if (jUnit5Case) { + const wrongNaming = !( + classInformation.className.endsWith('Test') || + classInformation.className.endsWith('Tests') + ) + if (wrongNaming) { + result.info = `${classInformation.className} class name do not match against class names ending in Test or Tests. This test may not function correctly.` + } + } + const className = !classInformation.packageName + ? classInformation.className + : classInformation.packageName + '.' + classInformation.className + return { + ...result, + filePath: path, + className + } +} diff --git a/settings.html b/settings.html index 943ee55..067bf0d 100644 --- a/settings.html +++ b/settings.html @@ -7,6 +7,7 @@ + @@ -38,6 +39,9 @@
+
+ Script will be executed isolated from user container click here +
@@ -152,8 +156,8 @@
- - + +
diff --git a/settings.js b/settings.js index 613079b..6a396f7 100644 --- a/settings.js +++ b/settings.js @@ -1,4 +1,38 @@ (function () { + const LANG_TYPES = { + JAVA: 'java', + CUSTOM: 'custom', + RUBY: 'ruby', + PYTHON: 'python', + JAVASCRIPT: 'javascript' + } + + const SUBTYPES_BY_LANG = { + [LANG_TYPES.JAVA]: { + STYLE: 'style', + JUNIT: 'junit' + }, + [LANG_TYPES.RUBY]: { + STYLE: 'style', + RSPEC: 'rspec' + }, + [LANG_TYPES.PYTHON]: { + STYLE: 'style', + UNITTEST: 'unittest' + }, + [LANG_TYPES.JAVASCRIPT]: { + JSHINT: 'jshint', + JSLINT: 'jslint' + } + } + + const EXTENSIONS_BY_TYPE = { + [LANG_TYPES.JAVA]: ['java'], + [LANG_TYPES.RUBY]: ['rb'], + [LANG_TYPES.PYTHON]: ['py'], + [LANG_TYPES.JAVASCRIPT]: ['js'] + } + const collectSettings = () => { const instructions = $('#instructions').val() const timeout = parseInt($('#timeout').val(), 10); @@ -13,7 +47,7 @@ const applySettings = (settings = {}) => { $('#instructions').val(settings.instructions || ''); - $('#timeout').val(settings.timeout || ''); + $('#timeout').val(settings.timeout || '40'); } const processMessage = (jsonData) => { @@ -43,6 +77,48 @@ onSubtypeChanged(language, subtype) } + const addParsedTestCase = (path, info) => { + + } + + const addTestCase = async (path) => { + if ($(`.test-case-item[data-path="${path}"]`)) { + // todo already added warning + return + } + try { + const {content} = await window.codioAssessmentsHelper.sendAndWait( + window.codioAssessmentsHelper.METHODS.GET_FILE_CONTENT, {path} + ) + const langType = $('#languageType').val() + if (EXTENSIONS_BY_TYPE[langType]) { + return + } + const subtype = $(`${langType}LangSubtype`).val() + const ext = path.split('.').pop() + if (langType === LANG_TYPES.JAVA && subtype === 'style' && ext === 'xml') { + $('#javaStyleConfigPath').val(path) + return + } + if (!EXTENSIONS_BY_TYPE[langType].includes(ext)) { + // todo error 'Incorrect file type, should be: ' + typeToExtension[type].join(' ') + return + } + if (LANG_TYPES.JAVA && subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { + try { + const info = window.codioTestAssessment.javaGrammar.getJavaInfo(path, content) + addParsedTestCase(path, info) + } catch (e) { + // todo show errors + } + } else { + addParsedTestCase(path) + } + } catch (e) { + // todo show error + } + } + const bindEvents = () => { $('#languageType').on('change', function () { const languageType = $(this).val(); @@ -52,24 +128,20 @@ const langSubtype = $(this).val(); onSubtypeChanged($('#languageType').val(), langSubtype); }) - - // $("html").on("dragover", function(event) { - // event.preventDefault(); - // event.stopPropagation(); - // console.log('dragover', event); - // }); - // - // $("html").on("dragleave", function(event) { - // event.preventDefault(); - // event.stopPropagation(); - // console.log('dragover', event); - // }); - - $("html").on("drop", function(event) { - event.preventDefault(); - event.stopPropagation(); - console.log('drop', event); - }); + $('#customCommand').on('input', function () { + const helpBlock = $('.secure-folder-help-block') + helpBlock.addClass('hide') + if (!$(this).val().includes('.guides/secure')) { + helpBlock.removeClass('hide') + } + }) + $('#newCasePath').on('input', function () { + const addCaseBtn = $('.add-case-btn') + addCaseBtn.prop('disabled', !$(this).val()) + }) + $('.add-case-btn').on('click', function () { + addTestCase($('#newCasePath').val()) + }) } const onLoad = async () => { From df2ce478d4317686d73b7ee3dc07f318c623120c Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Mon, 25 May 2026 14:11:31 +0100 Subject: [PATCH 04/10] add test cases --- settings.css | 47 +++++++++++++++++++++++++++++++++++++++ settings.html | 8 +++++-- settings.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/settings.css b/settings.css index 27da8ee..d6876a1 100644 --- a/settings.css +++ b/settings.css @@ -11,3 +11,50 @@ .test-case-add-case-container { flex: 1; } + +.test-case-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.test-case-item { + display: flex; + border: 1px solid rgba(0, 0, 0, 0.6); + padding: 10px; + gap: 10px; + border-radius: 6px; + align-items: center; +} + +.test-case-item-info { + flex: 1; +} + +.test-case-info-row { + display: flex; + gap: 6px; +} + +.test-case-info-label { + font-weight: 600; +} + +.test-case-item-actions { + display: flex; +} + +.test-case-delete-button { + background: none; + border: none; + padding: 0; + width: 24px; + height: 24px; + cursor: pointer; + opacity: 0.8; + color: red; + + &:hover { + opacity: 1; + } +} diff --git a/settings.html b/settings.html index 067bf0d..e6c03a9 100644 --- a/settings.html +++ b/settings.html @@ -153,13 +153,17 @@
-
+
- + +
+

Test cases

+
+ No test cases added
diff --git a/settings.js b/settings.js index 6a396f7..005a612 100644 --- a/settings.js +++ b/settings.js @@ -1,4 +1,6 @@ (function () { + const ICON_DELETE = '' + const LANG_TYPES = { JAVA: 'java', CUSTOM: 'custom', @@ -72,17 +74,67 @@ const onLanguageChanged = (language) => { $('.language-settings-container').addClass('hide') + const testsContainer = $('.test-case-container') + testsContainer.addClass('hide') + testsContainer.find('.test-case-list').empty() + if (language !== LANG_TYPES.CUSTOM) { + testsContainer.removeClass('hide') + } $(`.${language}-container`).removeClass('hide') const subtype = $(`#${language}LangSubtype`).val() onSubtypeChanged(language, subtype) + + addParsedTestCase({filePath: 'assessment-test.js', className: 'assessment-test'}); + addParsedTestCase({filePath: 'assessment-test.js', className: 'assessment-test'}); + addParsedTestCase({filePath: 'assessment-test.js', className: 'assessment-test'}); } - const addParsedTestCase = (path, info) => { + const addParsedTestCase = (info) => { + const path = info?.filePath || info + const className = info?.className + + const list = $('.test-case-list') + const itemContainer = $('
') + itemContainer.data('path', path) + const infoContainer = $('
') + const pathInfoRow = $('
') + pathInfoRow.append('
Path:
') + pathInfoRow.append(`
${path}
`) + infoContainer.append(pathInfoRow) + if (className) { + const classInfoRow = $('
') + classInfoRow.append('
Class name:
') + classInfoRow.append(`
${className}
`) + infoContainer.append(classInfoRow) + } + const actionsContainer = $('
') + const deleteBtn = $(``) + actionsContainer.append(deleteBtn) + itemContainer.append(infoContainer) + itemContainer.append(actionsContainer) + list.append(itemContainer) + updateTestsHelpBlockVisibility() + } + + const updateTestsHelpBlockVisibility = () => { + const list = $('.test-case-list') + const hasItems = !!list.find('.test-case-item')[0] + const helpBlock = $('.test-case-noItems-block') + hasItems ? helpBlock.addClass('hide') : helpBlock.removeClass('hide') + } + + const removeTestCase = (item) => { + // todo confirm removal + item.remove() + updateTestsHelpBlockVisibility() } const addTestCase = async (path) => { - if ($(`.test-case-item[data-path="${path}"]`)) { + console.log('add test case', path) + if ($(`.test-case-item[data-path="${path}"]`)[0]) { // todo already added warning return } @@ -107,7 +159,7 @@ if (LANG_TYPES.JAVA && subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { try { const info = window.codioTestAssessment.javaGrammar.getJavaInfo(path, content) - addParsedTestCase(path, info) + addParsedTestCase(info) } catch (e) { // todo show errors } @@ -142,6 +194,9 @@ $('.add-case-btn').on('click', function () { addTestCase($('#newCasePath').val()) }) + $('.test-case-list').on('click', '.test-case-delete-button', function () { + removeTestCase($(this).closest('.test-case-item')); + }) } const onLoad = async () => { From b928f667133ea227d0e89d3ff360dc4f4c149157 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 26 May 2026 16:39:06 +0100 Subject: [PATCH 05/10] collect config --- settings.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/settings.js b/settings.js index 005a612..8820cb8 100644 --- a/settings.js +++ b/settings.js @@ -35,11 +35,81 @@ [LANG_TYPES.JAVASCRIPT]: ['js'] } + const getCodeEnvConfig = () => { + const langType = $('#languageType').val() + + if (langType === LANG_TYPES.CUSTOM) { + return null + } + + const subtype = $(`#${langType}LangSubtype`).val() + const isJava = langType === LANG_TYPES.JAVA + + const files = [] + $(`.test-case-item`).each(function() { + const test = $(this) + const filePath = test.find('.test-case-info-path').find('.test-case-info-text').text() + if (isJava && subtype === SUBTYPES_BY_LANG.JAVA.JUNIT) { + const className = test.find('.test-case-info-class').find('.test-case-info-text').text() + files.push({filePath, className}) + } else { + files.push(filePath) + } + }) + + let envConfig = { + type: langType, + subtype, + files + } + if (isJava) { + if (subtype === SUBTYPES_BY_LANG.JAVA.JUNIT) { + const javaConfig = { + wd: $('#javaJunitWorkingDirectory').val(), + sources: $('#javaJunitSourcePath').val(), + libs: $('#javaJunitLibraryPath').val(), + testsources: $('#junitTestsSourcePath').val(), + jUnitVersion: $('#javaJunitVersion').val() + } + envConfig = {...envConfig, ...javaConfig} + } else if (subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { + envConfig = { + ...envConfig, + configFile: $('#javaStyleConfigPath').val(), + checkStyleVersion: $('#javaCheckStyleVersion').val() + } + } + } else if (langType === LANG_TYPES.PYTHON && subtype === SUBTYPES_BY_LANG.PYTHON.UNITTEST) { + envConfig = { + ...envConfig, + executable: $('#pythonUnittestExecutable').val(), + pythonwd: $('#pythonUnittestWorkingDirectory').val() + } + } + return envConfig + } + const collectSettings = () => { const instructions = $('#instructions').val() const timeout = parseInt($('#timeout').val(), 10); + const langType = $('#languageType').val() + const subtype = $(`#${langType}LangSubtype`).val() - return {instructions, timeout}; + const data = { + instructions, + timeout + } + + const pythonUnittestStudentFolder = $('#pythonUnittestStudentFolder').val() + if (langType === LANG_TYPES.PYTHON && subtype === SUBTYPES_BY_LANG.PYTHON.UNITTEST && pythonUnittestStudentFolder) { + data.pythonPath = pythonUnittestStudentFolder + } + if (langType === LANG_TYPES.CUSTOM) { + data.command = $('#customCommand').val() + } else { + data.codeEnvConfig = JSON.stringify(getCodeEnvConfig() || {}) + } + return data; } const exportSettings = () => { @@ -83,10 +153,6 @@ $(`.${language}-container`).removeClass('hide') const subtype = $(`#${language}LangSubtype`).val() onSubtypeChanged(language, subtype) - - addParsedTestCase({filePath: 'assessment-test.js', className: 'assessment-test'}); - addParsedTestCase({filePath: 'assessment-test.js', className: 'assessment-test'}); - addParsedTestCase({filePath: 'assessment-test.js', className: 'assessment-test'}); } const addParsedTestCase = (info) => { @@ -97,12 +163,12 @@ const itemContainer = $('
') itemContainer.data('path', path) const infoContainer = $('
') - const pathInfoRow = $('
') + const pathInfoRow = $('
') pathInfoRow.append('
Path:
') pathInfoRow.append(`
${path}
`) infoContainer.append(pathInfoRow) if (className) { - const classInfoRow = $('
') + const classInfoRow = $('
') classInfoRow.append('
Class name:
') classInfoRow.append(`
${className}
`) infoContainer.append(classInfoRow) @@ -146,7 +212,7 @@ ${ICON_DELETE} if (EXTENSIONS_BY_TYPE[langType]) { return } - const subtype = $(`${langType}LangSubtype`).val() + const subtype = $(`#${langType}LangSubtype`).val() const ext = path.split('.').pop() if (langType === LANG_TYPES.JAVA && subtype === 'style' && ext === 'xml') { $('#javaStyleConfigPath').val(path) From edb79f1ccb0060cc6cb10e26c35c9c739100bc75 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Wed, 27 May 2026 16:49:36 +0100 Subject: [PATCH 06/10] assessment rendering --- index.css | 0 index.html | 67 +++++++++++ index.js | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++ settings.js | 5 +- 4 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 index.css create mode 100644 index.html create mode 100644 index.js diff --git a/index.css b/index.css new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..ae959fe --- /dev/null +++ b/index.html @@ -0,0 +1,67 @@ + + + + + Advanced test assessment + + + + + + + + + + +
+

+
+
+
+
+
+
+ +
+
+ + diff --git a/index.js b/index.js new file mode 100644 index 0000000..597ef97 --- /dev/null +++ b/index.js @@ -0,0 +1,318 @@ +(function (){ + let assessmentOptions = null + let assessment = null + let processing = false + let currentData = null + let expanded = false + const LONG_OUTPUT_LENGTH = 20000 + + const updateProcessing = (status) => { + processing = status + refreshResultAndFooter() + } + + const applyStateInitial = (data) => { + const {state, result, ...dataWithoutState} = data + assessment = dataWithoutState.assessment + assessmentOptions = dataWithoutState.options + + render() + } + + const applyState = (data) => { + console.log('assessment iframe applyState', data) + currentData = data + if (!assessment) { + applyStateInitial(data) + return + } + updateCheckButtonText() + refreshResultAndFooter() + renderGuidance() + } + + const onCheck = (event) => { + event.preventDefault() + updateProcessing(true) + + window.codioAssessmentsHelper.send( + window.codioAssessmentsHelper.METHODS.SUBMIT_ANSWER + ) + } + + const onUnblock = (event) => { + event.preventDefault() + codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.UNBLOCK) + } + + const onReset = (event) => { + event.preventDefault() + codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.RESET) + } + + const renderContent = () => { + $('.instructions-text').html(assessment.source.settings.instructions) + } + + const updateVisibility = (el, visible) => { + visible ? el.removeClass('hide') : el.addClass('hide') + } + + const updateFooterButtons = () => { + const assessmentState = getAssessmentState() + const {teacherInStudentsProject, showModify, isDisabled, canAnswerAgain, answered} = assessmentState + + const checkVisibility = !showModify && assessmentOptions.useSubmitButtons + const checkBtn = $('.check-button') + updateVisibility(checkBtn, checkVisibility) + checkBtn.prop('disabled', isDisabled) + + const unblockVisibility = !teacherInStudentsProject && showModify + updateVisibility($('.unblock-button'), unblockVisibility) + + const state = processing ? window.codioAssessmentsHelper.States.PROGRESS : currentData?.result?.state + const resetVisibility = !showModify && answered && assessmentOptions.owner && + state !== window.codioAssessmentsHelper.States.PROGRESS && !canAnswerAgain + updateVisibility($('.reset-button'), resetVisibility) + } + + const updateCheckButtonText = () => { + const footerContainer = $('.codio-assessment-footer') + const {result} = currentData || {} + const caption = window.codioAssessmentsHelper.getButtonCaption( + assessmentOptions, + assessment.source.maxAttemptsCount, + result?.usedAttempts || 0 + ) + footerContainer.find('.check-button').html(caption) + } + + const renderGuidance = () => { + const guidanceBlock = $('.codio-assessment-guidance-block') + guidanceBlock.empty() + const assessmentState = getAssessmentState() + const {result} = currentData || {} + const guidance = window.codioAssessmentsHelper.calculateGuidance( + !assessmentOptions.eduStartedAssignment, + assessmentOptions.showAsTeacher, + assessmentState.answered, + assessment.source, + result ? + { + answerGuidance: result.guidance, + answerPoints: result.points, + attemptsCount: result.usedAttempts, + passed: result.state === window.codioAssessmentsHelper.States.PASS, + isCompletedAndReleased: window.codioAssessmentsHelper.calculateCompletedAndReleased( + assessmentOptions.eduStartedAssignment + ) + } : {} + ) + if (guidance) { + const guidanceContainer = $('
') + const guidanceText = $('
').html(guidance) + guidanceContainer.append(guidanceText) + guidanceBlock.append(guidanceContainer) + } + } + + const onExpandClick = () => { + const {EXPAND, COLLAPSE} = window.codioAssessmentsHelper.METHODS + const action = expanded ? COLLAPSE :EXPAND + expanded = !expanded + window.codioAssessmentsHelper.send(action) + } + + const isString = (val) => typeof val === 'string' || val instanceof String + + const renderFeedbackOutput = async (comment, format) => { + if (!isString(comment)) { + return null + } + if (format === window.codioAssessmentsHelper.SCRIPT_GRADE_FORMAT.MD) { + try { + const file = await unified() + .use(window.remarkParse) + .use(window.remarkRehype) + .use(window.rehypeExternalLinks, {rel: [], target: '_blank'}) + .use(window.rehypeSanitize) + .use(window.rehypeStringify) + .process(comment) + + return file.value + } catch (e) { + console.error(e) + } + } else if (format === window.codioAssessmentsHelper.SCRIPT_GRADE_FORMAT.HTML) { + return window.DOMPurify.sanitize(comment) + } + return comment.split('\n').map(item => { + return `

${window.codioAssessmentsHelper.escapeHTML(item)}

` + }).join('') + } + + const getFeedbackContainer = () => { + const container = $('
') + container.attr('aria-label', `Scrollable feedback ${assessment.source.showName ? assessment.source.name : ''}`) + return container + } + + const getValidHtml = (text) => { + return new DOMParser().parseFromString(text, 'text/html').querySelector("body").innerHTML + } + + const cutOutput = (inputText) => { + return inputText.length < LONG_OUTPUT_LENGTH ? inputText : inputText.substring(0, LONG_OUTPUT_LENGTH) + '\n...' + } + + const renderOutput = async (container) => { + const result = currentData?.result + if (result?.state === window.codioAssessmentsHelper.States.RESET) { + return + } + const outputEl = $('
') + if (isString(result?.feedback)) { + outputEl.removeClass('hide') + const feedbackContainer = getFeedbackContainer() + const feedback = await renderFeedbackOutput(result.feedback, result.format) + feedbackContainer.append(feedback) + outputEl.append(feedbackContainer) + if (result.code !== 0) { + const debugFeedbackContainer = getFeedbackContainer() + const debugFeedback = await renderFeedbackOutput(result.output) + debugFeedbackContainer.append(debugFeedback) + outputEl.append(debugFeedbackContainer) + } + return + } + if (isString(result?.output)) { + outputEl.removeClass('hide') + const outputContainer = getFeedbackContainer() + outputContainer.html(getValidHtml(cutOutput(result.output))) + outputEl.append(outputContainer) + } + container.append(outputEl) + } + + const renderResult = () => { + const assessmentState = getAssessmentState() + const resultBlock = $('.codio-assessment-results-block') + resultBlock.empty() + if (!assessmentState.answered && !processing) { + return + } + const result = currentData?.result + const state = processing ? window.codioAssessmentsHelper.States.PROGRESS : result?.state + const resultEl = $(`
`) + + const assessmentStatus = window.codioAssessmentsHelper.getAssessmentResultStatus( + assessment.source, result, processing + ) + const iconStr = window.codioAssessmentsHelper.getIconByResultStatus(assessmentStatus) + const iconEl = $(iconStr).addClass(`codio-assessment-result-status-icon ${assessmentStatus}`) + const iconContainer = $('
') + iconContainer.append(iconEl) + resultEl.append(iconContainer) + const resultInfoContainer = $('
') + + if (result?.timestamp) { + const timestamp = new Date(result.timestamp).toLocaleString() + const timestampEl = $(` +
LAST RUN  +on ${timestamp} +
+`) + resultInfoContainer.append(timestampEl) + } + renderOutput(resultInfoContainer) + resultEl.append(resultInfoContainer) + + const resultActionsContainer = $('
') + const expandButton = $(` + +`) + expandButton.attr('aria-label', `Expand output ${assessment.source.showName ? assessment.source.name : ''}`) + expandButton.on('click', onExpandClick) + resultActionsContainer.append(expandButton) + resultEl.append(resultActionsContainer) + resultBlock.append(resultEl) + } + + const getAssessmentState = () => { + const result = currentData ? currentData.result : null + const answered = result?.state && result?.state !== window.codioAssessmentsHelper.States.RESET + const usedAttempts = result?.usedAttempts || 0 + const canAnswerAgain = !assessment.source.maxAttemptsCount || usedAttempts < assessment.source.maxAttemptsCount + const showModify = assessmentOptions.showUnblock && (!answered || canAnswerAgain) + const isDisabled = processing || + assessmentOptions.isDisabled || + result?.state === window.codioAssessmentsHelper.States.PROGRESS || + answered && !canAnswerAgain + const teacherInStudentsProject = assessmentOptions.showAsTeacher && !assessmentOptions.owner + + return { + isDisabled, + answered, + usedAttempts, + canAnswerAgain, + showModify, + teacherInStudentsProject + } + } + + const refreshResultAndFooter = () => { + if (!assessment) { + return + } + renderResult() + updateFooterButtons() + } + + const bindEvents = () => { + $('.check-button').on('click', onCheck) + $('.unblock-button').on('click', onUnblock) + $('.reset-button').on('click', onReset) + + window.codioAssessmentsHelper.addBodyHeightListener() + } + + const render = () => { + const container = $('.codio-assessment') + const nameEl = container.find('.codio-assessment-name') + assessment.source.showName ? nameEl.text(assessment.source.name) : nameEl.remove() + renderContent() + updateCheckButtonText() + renderGuidance() + refreshResultAndFooter() + bindEvents() + container.removeClass('hide') + } + + const processMessage = (jsonData) => { + try { + const {method, data} = JSON.parse(jsonData) + console.log('assessment iframe processMessage', jsonData, method, data) + switch (method) { + case window.codioAssessmentsHelper.METHODS.GET_STYLES_RESPONSE: + window.codioAssessmentsHelper.addStyle(data.css) + break + case window.codioAssessmentsHelper.METHODS.GET_STATE_RESPONSE: + updateProcessing(false) + applyState(data) + break + case window.codioAssessmentsHelper.METHODS.CALLBACK: { + window.codioAssessmentsHelper.processCallback(data) + break + } + } + } catch {} + } + + window.addEventListener('load', () => { + window.codioAssessmentsHelper.registerMessageListener(processMessage) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_STATE) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_STYLES) + }) +})() diff --git a/settings.js b/settings.js index 8820cb8..38735f4 100644 --- a/settings.js +++ b/settings.js @@ -120,6 +120,8 @@ const applySettings = (settings = {}) => { $('#instructions').val(settings.instructions || ''); $('#timeout').val(settings.timeout || '40'); + const config = settings.codeEnvConfig ? JSON.parse(settings.codeEnvConfig) : null + onLanguageChanged(config?.type || LANG_TYPES.CUSTOM) } const processMessage = (jsonData) => { @@ -205,6 +207,7 @@ ${ICON_DELETE} return } try { + // todo need to implement get/set content for settings const {content} = await window.codioAssessmentsHelper.sendAndWait( window.codioAssessmentsHelper.METHODS.GET_FILE_CONTENT, {path} ) @@ -249,7 +252,7 @@ ${ICON_DELETE} $('#customCommand').on('input', function () { const helpBlock = $('.secure-folder-help-block') helpBlock.addClass('hide') - if (!$(this).val().includes('.guides/secure')) { + if ($(this).val().includes('.guides/secure')) { helpBlock.removeClass('hide') } }) From f49fadb32895ef588927f6f461e5730ee192c5d9 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Thu, 28 May 2026 15:31:02 +0100 Subject: [PATCH 07/10] fixed get files content --- settings.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/settings.js b/settings.js index 38735f4..b943c68 100644 --- a/settings.js +++ b/settings.js @@ -135,6 +135,10 @@ case window.codioAssessmentsHelper.METHODS.GET_SETTINGS_RESPONSE: applySettings(data.settings); break; + case window.codioAssessmentsHelper.METHODS.CALLBACK: { + window.codioAssessmentsHelper.processCallback(data) + break + } } } catch {} } @@ -212,7 +216,7 @@ ${ICON_DELETE} window.codioAssessmentsHelper.METHODS.GET_FILE_CONTENT, {path} ) const langType = $('#languageType').val() - if (EXTENSIONS_BY_TYPE[langType]) { + if (!EXTENSIONS_BY_TYPE[langType]) { return } const subtype = $(`#${langType}LangSubtype`).val() @@ -222,6 +226,7 @@ ${ICON_DELETE} return } if (!EXTENSIONS_BY_TYPE[langType].includes(ext)) { + console.error(`Ext ${ext} not supported`) // todo error 'Incorrect file type, should be: ' + typeToExtension[type].join(' ') return } From 8531642e5ffb0010d93e5b03511fbb05c40b6e9b Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Thu, 28 May 2026 17:00:53 +0100 Subject: [PATCH 08/10] fixed collect setting --- settings.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/settings.js b/settings.js index b943c68..38d1b7a 100644 --- a/settings.js +++ b/settings.js @@ -10,19 +10,19 @@ } const SUBTYPES_BY_LANG = { - [LANG_TYPES.JAVA]: { + JAVA: { STYLE: 'style', JUNIT: 'junit' }, - [LANG_TYPES.RUBY]: { + RUBY: { STYLE: 'style', RSPEC: 'rspec' }, - [LANG_TYPES.PYTHON]: { + PYTHON: { STYLE: 'style', UNITTEST: 'unittest' }, - [LANG_TYPES.JAVASCRIPT]: { + JAVASCRIPT: { JSHINT: 'jshint', JSLINT: 'jslint' } @@ -35,6 +35,8 @@ [LANG_TYPES.JAVASCRIPT]: ['js'] } + const TEST_TIMEOUT = 300 + const getCodeEnvConfig = () => { const langType = $('#languageType').val() @@ -108,6 +110,8 @@ data.command = $('#customCommand').val() } else { data.codeEnvConfig = JSON.stringify(getCodeEnvConfig() || {}) + data.command = 'python /usr/share/codio/assessments/assessments.py' + data.timeout = TEST_TIMEOUT } return data; } @@ -230,7 +234,7 @@ ${ICON_DELETE} // todo error 'Incorrect file type, should be: ' + typeToExtension[type].join(' ') return } - if (LANG_TYPES.JAVA && subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { + if (langType === LANG_TYPES.JAVA && subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { try { const info = window.codioTestAssessment.javaGrammar.getJavaInfo(path, content) addParsedTestCase(info) From 8b7e3f464fea446b891c125b8645f28205cc2d28 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 2 Jun 2026 16:52:30 +0100 Subject: [PATCH 09/10] refactor settings --- settings.html | 72 ++++++++++++++++++++++++++++++++++----------------- settings.js | 10 +++++-- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/settings.html b/settings.html index e6c03a9..ae3a31b 100644 --- a/settings.html +++ b/settings.html @@ -12,9 +12,11 @@
-
+
- +
+ +
@@ -29,15 +31,19 @@
-
+
- +
+ +
-
+
- +
+ +
Script will be executed isolated from user container click here @@ -65,9 +71,11 @@
-
+
- +
+ +
@@ -94,24 +102,32 @@
-
+
- +
+ +
-
+
- +
+ +
-
+
- +
+ +
-
+
- +
+ +
@@ -127,18 +143,24 @@
-
+
- +
+ +
-
+
- +
+ +
-
+
- +
+ +
@@ -155,9 +177,11 @@
-
+
- +
+ +
diff --git a/settings.js b/settings.js index 38d1b7a..0be6c3f 100644 --- a/settings.js +++ b/settings.js @@ -92,6 +92,7 @@ } const collectSettings = () => { + const errors = [] const instructions = $('#instructions').val() const timeout = parseInt($('#timeout').val(), 10); const langType = $('#languageType').val() @@ -108,12 +109,17 @@ } if (langType === LANG_TYPES.CUSTOM) { data.command = $('#customCommand').val() + !data.command && errors.push('Command field must be completed') } else { - data.codeEnvConfig = JSON.stringify(getCodeEnvConfig() || {}) + const config = getCodeEnvConfig() || {} + data.codeEnvConfig = JSON.stringify(config) data.command = 'python /usr/share/codio/assessments/assessments.py' data.timeout = TEST_TIMEOUT + if (!config.files?.length) { + errors.push('Add a test case') + } } - return data; + return {data, errors}; } const exportSettings = () => { From 919fdd45fd45d0374cc63eeb865c08241e2a46ef Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 9 Jun 2026 12:37:48 +0100 Subject: [PATCH 10/10] tiny mde as markdown editor --- settings.html | 1 + settings.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/settings.html b/settings.html index ae3a31b..872d5a3 100644 --- a/settings.html +++ b/settings.html @@ -15,6 +15,7 @@
+
diff --git a/settings.js b/settings.js index 0be6c3f..8da93cb 100644 --- a/settings.js +++ b/settings.js @@ -1,5 +1,6 @@ (function () { const ICON_DELETE = '' + let instructionsEditor = null const LANG_TYPES = { JAVA: 'java', @@ -93,7 +94,7 @@ const collectSettings = () => { const errors = [] - const instructions = $('#instructions').val() + const instructions = instructionsEditor.getContent() const timeout = parseInt($('#timeout').val(), 10); const langType = $('#languageType').val() const subtype = $(`#${langType}LangSubtype`).val() @@ -128,7 +129,7 @@ } const applySettings = (settings = {}) => { - $('#instructions').val(settings.instructions || ''); + instructionsEditor.setContent(settings.instructions || '') $('#timeout').val(settings.timeout || '40'); const config = settings.codeEnvConfig ? JSON.parse(settings.codeEnvConfig) : null onLanguageChanged(config?.type || LANG_TYPES.CUSTOM) @@ -288,6 +289,7 @@ ${ICON_DELETE} window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS) bindEvents() + instructionsEditor = window.codioAssessmentsHelper.initializeMarkdownEditor('instructions', 'instructions-command-bar') } window.addEventListener('load', onLoad);