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/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/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/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..d6876a1 --- /dev/null +++ b/settings.css @@ -0,0 +1,60 @@ +.settings-content-row-container { + display: flex; + flex-direction: row; + gap: 10px; +} + +.test-case-controls-container { + align-items: end; +} + +.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 new file mode 100644 index 0000000..872d5a3 --- /dev/null +++ b/settings.html @@ -0,0 +1,197 @@ + + + + + Settings + + + + + + + + +
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ Script will be executed isolated from user container click here +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+ +
+

Test cases

+
+ No test cases added +
+
+
+
+ + diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..8da93cb --- /dev/null +++ b/settings.js @@ -0,0 +1,296 @@ +(function () { + const ICON_DELETE = '' + let instructionsEditor = null + + const LANG_TYPES = { + JAVA: 'java', + CUSTOM: 'custom', + RUBY: 'ruby', + PYTHON: 'python', + JAVASCRIPT: 'javascript' + } + + const SUBTYPES_BY_LANG = { + JAVA: { + STYLE: 'style', + JUNIT: 'junit' + }, + RUBY: { + STYLE: 'style', + RSPEC: 'rspec' + }, + PYTHON: { + STYLE: 'style', + UNITTEST: 'unittest' + }, + 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 TEST_TIMEOUT = 300 + + 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 errors = [] + const instructions = instructionsEditor.getContent() + const timeout = parseInt($('#timeout').val(), 10); + const langType = $('#languageType').val() + const subtype = $(`#${langType}LangSubtype`).val() + + 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() + !data.command && errors.push('Command field must be completed') + } else { + 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, errors}; + } + + const exportSettings = () => { + const data = collectSettings(); + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS_RESPONSE, data); + } + + const applySettings = (settings = {}) => { + instructionsEditor.setContent(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) => { + 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; + case window.codioAssessmentsHelper.METHODS.CALLBACK: { + window.codioAssessmentsHelper.processCallback(data) + break + } + } + } 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') + 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) + } + + 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) => { + console.log('add test case', path) + if ($(`.test-case-item[data-path="${path}"]`)[0]) { + // todo already added warning + return + } + try { + // todo need to implement get/set content for settings + 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)) { + console.error(`Ext ${ext} not supported`) + // todo error 'Incorrect file type, should be: ' + typeToExtension[type].join(' ') + return + } + if (langType === LANG_TYPES.JAVA && subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { + try { + const info = window.codioTestAssessment.javaGrammar.getJavaInfo(path, content) + addParsedTestCase(info) + } catch (e) { + // todo show errors + } + } else { + addParsedTestCase(path) + } + } catch (e) { + // todo show error + } + } + + 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); + }) + $('#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()) + }) + $('.test-case-list').on('click', '.test-case-delete-button', function () { + removeTestCase($(this).closest('.test-case-item')); + }) + } + + const onLoad = async () => { + window.codioAssessmentsHelper.registerMessageListener(processMessage) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS) + + bindEvents() + instructionsEditor = window.codioAssessmentsHelper.initializeMarkdownEditor('instructions', 'instructions-command-bar') + } + + window.addEventListener('load', onLoad); +})()