diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a09c56d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/.idea
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..8c30b9e
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,18 @@
+
diff --git a/index.css b/index.css
new file mode 100644
index 0000000..94a153e
--- /dev/null
+++ b/index.css
@@ -0,0 +1,7 @@
+.codio-assessment-output-diff-del {
+ background-color: #ffe6e6;
+}
+
+.codio-assessment-output-diff-ins {
+ background-color: #e9fff2;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..c6f32bd
--- /dev/null
+++ b/index.html
@@ -0,0 +1,34 @@
+
+
+
+ ').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 getValidHtml = (text) => {
+ return new DOMParser().parseFromString(text, 'text/html').querySelector("body").innerHTML
+ }
+
+ const diffOutput = (expectedOutput, output) => {
+ const dmp = new window.DiffMatchPatch()
+ const pattern_para = /\n/g
+ const diff = dmp.diff_main(output, expectedOutput)
+ dmp.diff_cleanupSemantic(diff)
+ const diffItems = diff.map(item => {
+ const text = item[1].replace(pattern_para, '¶
')
+ switch (item[0]) {
+ case 1:
+ return `
${getValidHtml(text)}`
+ case -1:
+ return `
${getValidHtml(text)}`
+ case 0:
+ default:
+ return getValidHtml(text)
+ }
+ })
+
+ return `
`
+ }
+
+ const renderOutput = () => {
+ const {showAsTeacher, eduStartedAssignment} = assessmentOptions
+ const result = currentData?.result
+ const currentState = processing ? window.codioAssessmentsHelper.States.PROGRESS : result?.state
+ const resultClasses = `codio-assessment-result-output codio-assessment-result-output-${currentState}`
+ let output = null
+ let currentLimit = LONG_OUTPUT_LENGTH
+
+ function cutOutput(inputText) {
+ return inputText.length < currentLimit ? inputText : inputText.substring(0, currentLimit) + '\n...'
+ }
+
+ if (result?.output) {
+ try {
+ const parsed = JSON.parse(result.output)
+ const sequenceSize = parsed?.sequence.length || 0
+ if (sequenceSize > 0) {
+ currentLimit = LONG_OUTPUT_LENGTH / sequenceSize
+ }
+ if (parsed.sequence) {
+ output = parsed.sequence.map((item, pos) => {
+ let state = `
failed`
+ let reason = ''
+ let feedback = ''
+ if (item && item.passed) {
+ state = `
passed`
+ } else {
+ let expectedStr = ''
+ const showExpectedAnswer = window.codioAssessmentsHelper.calculateShowExpectedAnswer(
+ eduStartedAssignment,
+ assessment.source.showExpectedAnswerOption
+ )
+ const showAnswerFromSource = showAsTeacher || showExpectedAnswer
+ const sourceSequence = assessment.source.sequence && assessment.source.sequence[pos]
+ const sourceExpectedOutput = assessment.source.expectedOutputs &&
+ !isEmptyObject(assessment.source.expectedOutputs[pos]) &&
+ assessment.source.expectedOutputs[pos]
+ const resultExpectedOutput = !assessment.source.sequence &&
+ result.expectedOutputs && !isEmptyObject(result.expectedOutputs[pos]) && result.expectedOutputs[pos]
+ const expectedResult = (showAnswerFromSource && (sourceSequence || sourceExpectedOutput)) ||
+ resultExpectedOutput
+ if (expectedResult) {
+ if (showAsDiff) {
+ const truncatedOutput = cutOutput(item.stdout + item.stderr)
+ reason = diffOutput(expectedResult.output, truncatedOutput)
+ } else {
+ const escapedExpectedOutput = window.codioAssessmentsHelper.escapeHTML(expectedResult.output)
+ expectedStr = `Expected:
${escapedExpectedOutput}
`
+
+ const outputText = cutOutput(window.codioAssessmentsHelper.escapeHTML(item.stdout + item.stderr))
+ const outputStr = `Output:
${outputText}
`
+
+ reason = `
${outputStr}${expectedStr}`
+ }
+ }
+ }
+ if (!item.passed) {
+ const sourceSequence = assessment.source.sequence && assessment.source.sequence[pos]
+ const sourceFeedback = sourceSequence && sourceSequence.showFeedback && sourceSequence.feedback
+ const resultFeedbackObject = !assessment.source.sequence && assessment.source.feedbacks &&
+ assessment.source.feedbacks[pos]
+ const feedbackToShow = sourceFeedback || (resultFeedbackObject.show && resultFeedbackObject.feedback)
+ if (feedbackToShow && !showAsDiff) {
+ const escapedFeedback = window.codioAssessmentsHelper.escapeHTML(feedbackToShow)
+ feedback = `
Feedback:${escapedFeedback}
`
+ }
+ }
+ return `
Check ${pos + 1} ${state}${reason}${feedback}
`
+ }).join('')
+ } else if (parsed.error) {
+ const errorOutput = cutOutput(parsed.error)
+ output = `
Error: ${errorOutput}
`
+ }
+ } catch {
+ output = cutOutput(result.output)
+ }
+ }
+ const outputEl = $(`
`)
+ outputEl.attr('tabIndex', 0)
+ outputEl.attr('aria-label', `Scrollable feedback ${assessment.source.showName ? assessment.source.name : ''}`)
+ outputEl.html(result?.state !== window.codioAssessmentsHelper.States.RESET ? output : '')
+
+ return 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)
+ }
+ resultInfoContainer.append(renderOutput())
+ 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 onToggleDiffView = () => {
+ showAsDiff = !showAsDiff
+ renderResult()
+ updateFooterButtons()
+ }
+
+ const refreshResultAndFooter = () => {
+ if (!assessment) {
+ return
+ }
+ renderResult()
+ updateFooterButtons()
+ }
+
+ const bindEvents = () => {
+ $('.check-button').on('click', onCheck)
+ $('.unblock-button').on('click', onUnblock)
+ $('.reset-button').on('click', onReset)
+ $('.diff-button').on('click', onToggleDiffView)
+
+ 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/metadata.json b/metadata.json
new file mode 100644
index 0000000..e9b1d2c
--- /dev/null
+++ b/metadata.json
@@ -0,0 +1,19 @@
+{
+ "name": "Standard Code Test",
+ "type": "assessment",
+ "properties": {
+ "type": "code-output-compare",
+ "icon": "./icon.svg",
+ "defaultHeight": 500,
+ "gradingControls": {
+ "points": true,
+ "allowPartialPoints": true,
+ "useMaximumScore": true,
+ "commandOptions": true,
+ "testSequence": true,
+ "showExpectedAnswer": true,
+ "definedNumberOfAttempts": true,
+ "rationale": true
+ }
+ }
+}
diff --git a/settings.css b/settings.css
new file mode 100644
index 0000000..3d6e420
--- /dev/null
+++ b/settings.css
@@ -0,0 +1,5 @@
+.settings-commands {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+}
diff --git a/settings.html b/settings.html
new file mode 100644
index 0000000..f17c4b0
--- /dev/null
+++ b/settings.html
@@ -0,0 +1,49 @@
+
+
+
+
+
Settings
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.js b/settings.js
new file mode 100644
index 0000000..1ccd9b8
--- /dev/null
+++ b/settings.js
@@ -0,0 +1,55 @@
+(function () {
+ const DEFAULT_TIMEOUT = 30
+ let instructionsEditor = null
+
+ const collectSettings = () => {
+ const errors = []
+ const instructions = instructionsEditor.getContent()
+ const command = $('#command').val();
+ const preExecuteCommand = $('#preExecCommand').val();
+ const timeout = parseInt($('#timeout').val(), 10);
+
+ !instructions && errors.push('Instructions field must be completed');
+ !command && errors.push('Command field must be completed');
+
+ return {
+ data: {instructions, command, preExecuteCommand, timeout: timeout || DEFAULT_TIMEOUT},
+ errors
+ };
+ }
+
+ const exportSettings = () => {
+ const data = collectSettings();
+ window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS_RESPONSE, data);
+ }
+
+ const applySettings = (settings = {}) => {
+ instructionsEditor.setContent(settings.instructions || '')
+ $('#command').val(settings.command || '');
+ $('#preExecCommand').val(settings.preExecuteCommand || '');
+ $('#timeout').val(settings.timeout || DEFAULT_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)
+ instructionsEditor = window.codioAssessmentsHelper.initializeMarkdownEditor('instructions', 'instructions-command-bar')
+ }
+
+ window.addEventListener('load', onLoad);
+})()