From 1137ee556c70224f50d6fb65698e21db3c5479a8 Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Thu, 9 Mar 2017 11:12:57 +0100 Subject: [PATCH] OO-2602: skip close step if there are no review and no feedbacks, implement some selenium tests to secure the different flows for different settings --- pom.xml | 2 +- .../olat/ims/qti21/AssessmentTestSession.java | 2 + .../ui/AssessmentTestDisplayController.java | 125 ++++++- .../olat/ims/qti21/ui/_content/suspended.html | 15 +- .../AssessmentObjectComponentRenderer.java | 2 +- .../components/AssessmentRenderFunctions.java | 16 + .../components/AssessmentTestComponent.java | 5 +- .../AssessmentTestComponentRenderer.java | 25 +- .../org/olat/selenium/AssessmentTest.java | 23 +- .../java/org/olat/selenium/ImsQTI21Test.java | 344 +++++++++++++++++- src/test/java/org/olat/selenium/UserTest.java | 7 +- .../org/olat/selenium/page/qti/QTI21Page.java | 88 ++++- .../qti21/test_parts_without_feedbacks.zip | Bin 0 -> 5996 bytes .../file_resources/qti21/test_time_limits.zip | Bin 0 -> 5406 bytes .../qti21/test_with_feedbacks.zip | Bin 0 -> 4401 bytes .../test_with_parts_and_test_feedbacks.zip | Bin 0 -> 6221 bytes .../qti21/test_without_feedbacks.zip | Bin 0 -> 3599 bytes 17 files changed, 580 insertions(+), 74 deletions(-) create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip diff --git a/pom.xml b/pom.xml index 187f8a37c3f..1e1fc74575d 100644 --- a/pom.xml +++ b/pom.xml @@ -1997,7 +1997,7 @@ <dependency> <groupId>org.openolat.imscp</groupId> <artifactId>manifest</artifactId> - <version>1.3-SNAPSHOT</version> + <version>1.3.0</version> </dependency> <dependency> <groupId>rome</groupId> diff --git a/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java b/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java index 5cd84eb67c3..c88f11fb2d9 100644 --- a/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java +++ b/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java @@ -72,6 +72,8 @@ public interface AssessmentTestSession extends CreateInfo, ModifiedInfo { public boolean isExploded(); + public void setExploded(boolean exploded); + public String getStorage(); diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java index d2617ff2973..f3366585906 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java @@ -19,6 +19,8 @@ */ package org.olat.ims.qti21.ui; +import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFeedbackVisible; + import java.io.File; import java.math.BigDecimal; import java.net.URI; @@ -109,6 +111,8 @@ import uk.ac.ed.ph.jqtiplus.node.result.TestResult; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode; import uk.ac.ed.ph.jqtiplus.node.test.SubmissionMode; +import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback; +import uk.ac.ed.ph.jqtiplus.node.test.TestFeedbackAccess; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; import uk.ac.ed.ph.jqtiplus.notification.NotificationLevel; import uk.ac.ed.ph.jqtiplus.notification.NotificationRecorder; @@ -658,7 +662,7 @@ public class AssessmentTestDisplayController extends BasicController implements TestPlanNodeKey itemNodeKey = testSessionState.getCurrentItemKey(); if(itemNodeKey != null) { TestPlanNode currentItemNode = testSessionState.getTestPlan().getNode(itemNodeKey); - boolean hasFeedbacks = qtiWorksCtrl.willShowSomeFeedbacks(currentItemNode); + boolean hasFeedbacks = qtiWorksCtrl.willShowSomeAssessmentItemFeedbacks(currentItemNode); //allow skipping if(!hasFeedbacks) { processNextItem(ureq); @@ -999,6 +1003,40 @@ public class AssessmentTestDisplayController extends BasicController implements if(nextTestPart == null) { candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp, getDigitalSignatureOptions(), getIdentity()); + if(!qtiWorksCtrl.willShowSomeAssessmentTestFeedbacks()) { + //need feedback, no more parts, quickly exit + try { + //end current test part + testSessionController.enterNextAvailableTestPart(requestTimestamp); + } catch (final QtiCandidateStateException e) { + candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e); + logError("CANNOT_ADVANCE_TEST_PART", e); + return; + } catch (final RuntimeException e) { + candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e); + logError("RuntimeException", e); + return;// handleExplosion(e, candidateSession); + } + + //exit the test + NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); + CandidateTestEventType eventType = CandidateTestEventType.EXIT_TEST; + testSessionController.exitTest(requestTimestamp); + candidateSession.setTerminationTime(requestTimestamp); + candidateSession = qtiService.updateAssessmentTestSession(candidateSession); + + /* Record and log event */ + final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, + eventType, testSessionState, notificationRecorder); + candidateAuditLogger.logCandidateEvent(candidateTestEvent); + this.lastEvent = candidateTestEvent; + + qtiWorksCtrl.updateStatusAndResults(ureq); + doExitTest(ureq); + } + } else if(!qtiWorksCtrl.willShowSomeTestPartFeedbacks()) { + //no feedback, go to the next part + processAdvanceTestPart(ureq); } } @@ -1366,10 +1404,14 @@ public class AssessmentTestDisplayController extends BasicController implements BadResourceException ex = resolvedAssessmentTest.getTestLookup().getBadResourceException(); if(ex instanceof QtiXmlInterpretationException) { - QtiXmlInterpretationException exml = (QtiXmlInterpretationException)ex; - System.out.println(exml.getInterpretationFailureReason()); - for(QtiModelBuildingError err :exml.getQtiModelBuildingErrors()) { - System.out.println(err); + try {//try to log some informations + QtiXmlInterpretationException exml = (QtiXmlInterpretationException)ex; + logError(exml.getInterpretationFailureReason().toString(), null); + for(QtiModelBuildingError err :exml.getQtiModelBuildingErrors()) { + logError(err.toString(), null); + } + } catch (Exception e) { + logError("", e); } } @@ -1525,7 +1567,7 @@ public class AssessmentTestDisplayController extends BasicController implements updateStatusAndResults(ureq); } - public boolean willShowSomeFeedbacks(TestPlanNode itemNode) { + public boolean willShowSomeAssessmentItemFeedbacks(TestPlanNode itemNode) { if(itemNode == null || testSessionController == null || testSessionController.getTestSessionState().isExited() || testSessionController.getTestSessionState().isEnded()) { @@ -1534,6 +1576,77 @@ public class AssessmentTestDisplayController extends BasicController implements return qtiEl.getComponent().willShowFeedbacks(itemNode); } + + /** + * + * @return + */ + public boolean willShowSomeAssessmentTestFeedbacks() { + if(testSessionController == null + || testSessionController.getTestSessionState().isExited() + || testSessionController.getTestSessionState().isEnded()) { + return true; + } + + TestSessionState testSessionState = testSessionController.getTestSessionState(); + TestPlanNodeKey currentTestPartNodeKey = testSessionState.getCurrentTestPartKey(); + TestPlanNode currentTestPlanNode = testSessionState.getTestPlan().getNode(currentTestPartNodeKey); + boolean hasReviewableItems = currentTestPlanNode.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF) + .stream().anyMatch(itemNode + -> itemNode.getEffectiveItemSessionControl().isAllowReview() + || itemNode.getEffectiveItemSessionControl().isShowFeedback()); + if(hasReviewableItems) { + return true; + } + + //Show 'atEnd' test feedback f there's only 1 testPart + List<TestFeedback> testFeedbacks = qtiEl.getComponent().getAssessmentTest().getTestFeedbacks(); + for(TestFeedback testFeedback:testFeedbacks) { + if(testFeedback.getTestFeedbackAccess() == TestFeedbackAccess.AT_END + && testFeedbackVisible(testFeedback, testSessionController.getTestSessionState())) { + return true; + } + } + + return false; + } + + /** + * + * Check if the current test part will show some test part feedback, + * item feedback or item reviews. + * + * @return + */ + public boolean willShowSomeTestPartFeedbacks() { + if(testSessionController == null + || testSessionController.getTestSessionState().isExited() + || testSessionController.getTestSessionState().isEnded()) { + return true; + } + + TestSessionState testSessionState = testSessionController.getTestSessionState(); + TestPlanNodeKey currentTestPartNodeKey = testSessionState.getCurrentTestPartKey(); + TestPlanNode currentTestPlanNode = testSessionState.getTestPlan().getNode(currentTestPartNodeKey); + boolean hasReviewableItems = currentTestPlanNode.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF) + .stream().anyMatch(itemNode + -> itemNode.getEffectiveItemSessionControl().isAllowReview() + || itemNode.getEffectiveItemSessionControl().isShowFeedback()); + if(hasReviewableItems) { + return true; + } + + TestPart currentTestPart = testSessionController.getCurrentTestPart(); + List<TestFeedback> testFeedbacks = currentTestPart.getTestFeedbacks(); + for(TestFeedback testFeedback:testFeedbacks) { + if(testFeedback.getTestFeedbackAccess() == TestFeedbackAccess.AT_END + && testFeedbackVisible(testFeedback, testSessionController.getTestSessionState())) { + return true; + } + } + + return false; + } @Override protected Identifier getResponseIdentifierFromUniqueId(String uniqueId) { diff --git a/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html b/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html index c0282a63625..bcd24f92b66 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html +++ b/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html @@ -1,3 +1,16 @@ <div id="o_qti_container"> <div class="o_important">$r.translate("assessment.test.suspended")</div> -</div> \ No newline at end of file +</div> +<script type="text/javascript"> +/* <![CDATA[ */ +jQuery(function() { + try { + if(!(typeof window.qti21TestTimer === "undefined")) { + window.qti21TestTimer.cancel(); + } + } catch(e) { + if(window.console) console.log(e); + } +}); +/* ]]> */ +</script> \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java index c2124c674dd..dba3dc47be2 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java @@ -197,7 +197,7 @@ public abstract class AssessmentObjectComponentRenderer extends DefaultComponent } protected void renderTerminated(StringOutput sb, Translator translator) { - sb.append("<div class='o_info'>").append(translator.translate("terminated.msg")).append("</div>"); + sb.append("<div class='o_info o_sel_assessment_test_terminated'>").append(translator.translate("terminated.msg")).append("</div>"); } protected void renderItemStatus(StringOutput sb, ItemSessionState itemSessionState, RenderingRequest options, Translator translator) { diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java index d6928f5fde1..fa42cc925c4 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java @@ -49,9 +49,11 @@ import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.Choice; import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; import uk.ac.ed.ph.jqtiplus.node.item.template.declaration.TemplateDeclaration; +import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback; import uk.ac.ed.ph.jqtiplus.node.test.VisibilityMode; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; import uk.ac.ed.ph.jqtiplus.state.ItemSessionState; +import uk.ac.ed.ph.jqtiplus.state.TestSessionState; import uk.ac.ed.ph.jqtiplus.types.Identifier; import uk.ac.ed.ph.jqtiplus.types.ResponseData; import uk.ac.ed.ph.jqtiplus.types.StringResponseData; @@ -623,4 +625,18 @@ public class AssessmentRenderFunctions { String relativePath = component.relativePathTo(resolvedAssessmentItem); return component.getMapperUri() + "/file?href=" + relativePath + (uri == null ? "" : uri); } + + public static final boolean testFeedbackVisible(TestFeedback testFeedback, TestSessionState testSessionState) { + //<xsl:variable name="identifierMatch" select="boolean(qw:value-contains(qw:get-test-outcome-value(@outcomeIdentifier), @identifier))" as="xs:boolean"/> + Identifier outcomeIdentifier = testFeedback.getOutcomeIdentifier(); + Value outcomeValue = testSessionState.getOutcomeValue(outcomeIdentifier); + boolean identifierMatch = valueContains(outcomeValue, testFeedback.getOutcomeValue()); + //<xsl:if test="($identifierMatch and @showHide='show') or (not($identifierMatch) and @showHide='hide')"> + if((identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.SHOW_IF_MATCH) + || (!identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.HIDE_IF_MATCH)) { + return true; + } + return false; + } + } \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java index 3e6fc43326a..cc0f9371546 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java @@ -163,9 +163,7 @@ public class AssessmentTestComponent extends AssessmentObjectComponent { if(!itemSessionState.isResponded()) { return true; } - if(!itemNode.getEffectiveItemSessionControl().isAllowSkipping()) { - return true; - } + List<Interaction> interactions = assessmentItem.getItemBody().findInteractions(); for(Interaction interaction:interactions) { @@ -178,7 +176,6 @@ public class AssessmentTestComponent extends AssessmentObjectComponent { } ItemProcessingContext itemContext = getTestSessionController().getItemProcessingContext(itemNode); - if(assessmentItem.getItemBody().willShowFeedback(itemContext)) { return true; } diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java index a4e3d8565de..7b582e7278b 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java @@ -20,7 +20,7 @@ package org.olat.ims.qti21.ui.components; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.contentAsString; -import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.valueContains; +import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFeedbackVisible; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -72,7 +72,6 @@ import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedbackAccess; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; -import uk.ac.ed.ph.jqtiplus.node.test.VisibilityMode; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; import uk.ac.ed.ph.jqtiplus.running.TestSessionController; import uk.ac.ed.ph.jqtiplus.state.AssessmentSectionSessionState; @@ -455,9 +454,9 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe renderTestFeebacks(renderer, sb, currentTestPart.getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator); //Show 'atEnd' test feedback f there's only 1 testPart - if(!component.hasMultipleTestParts()) { - renderTestFeebacks(renderer, sb, component.getAssessmentTest().getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator); - } + //if(!component.hasMultipleTestParts()) { + renderTestFeebacks(renderer, sb, component.getAssessmentTest().getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator); + //} //test part review component.getTestSessionController().getTestSessionState().getTestPlan() @@ -579,24 +578,20 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe private void renderTestFeeback(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestFeedback testFeedback, URLBuilder ubu, Translator translator) { - //<xsl:variable name="identifierMatch" select="boolean(qw:value-contains(qw:get-test-outcome-value(@outcomeIdentifier), @identifier))" as="xs:boolean"/> - Identifier outcomeIdentifier = testFeedback.getOutcomeIdentifier(); - Value outcomeValue = component.getTestSessionController().getTestSessionState().getOutcomeValue(outcomeIdentifier); - boolean identifierMatch = valueContains(outcomeValue, testFeedback.getOutcomeValue()); - //<xsl:if test="($identifierMatch and @showHide='show') or (not($identifierMatch) and @showHide='hide')"> - if((identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.SHOW_IF_MATCH) - || (!identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.HIDE_IF_MATCH)) { - - sb.append("<h2>"); + TestSessionState testSessionState = component.getTestSessionController().getTestSessionState(); + if(testFeedbackVisible(testFeedback, testSessionState)) { + sb.append("<div class='o_info clearfix'>"); + sb.append("<h3>"); if(StringHelper.containsNonWhitespace(testFeedback.getTitle())) { sb.append(StringHelper.escapeHtml(testFeedback.getTitle())); } else { sb.append(translator.translate("assessment.test.modal.feedback")); } - sb.append("</h2>"); + sb.append("</h3>"); testFeedback.getChildren().forEach((flow) -> renderFlow(renderer, sb, component, null, null, flow, ubu, translator)); + sb.append("</div>"); } } diff --git a/src/test/java/org/olat/selenium/AssessmentTest.java b/src/test/java/org/olat/selenium/AssessmentTest.java index 796cb70f692..c081ddb8092 100644 --- a/src/test/java/org/olat/selenium/AssessmentTest.java +++ b/src/test/java/org/olat/selenium/AssessmentTest.java @@ -36,7 +36,6 @@ import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; -import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; import org.olat.selenium.page.LoginPage; @@ -69,7 +68,6 @@ import org.olat.user.restapi.UserVO; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.firefox.FirefoxDriver; /** * @@ -105,9 +103,6 @@ public class AssessmentTest { public void qti12Test(@InitialPage LoginPage authorLoginPage) throws IOException, URISyntaxException { - //File upload only work with Firefox - Assume.assumeTrue(browser instanceof FirefoxDriver); - UserVO author = new UserRestClient(deploymentUrl).createAuthor(); authorLoginPage.loginAs(author.getLogin(), author.getPassword()); @@ -200,9 +195,7 @@ public class AssessmentTest { public void qti12CourseWithAssessment(@InitialPage LoginPage authorLoginPage, @Drone @User WebDriver ryomouBrowser) throws IOException, URISyntaxException { - //File upload only work with Firefox - Assume.assumeTrue(browser instanceof FirefoxDriver); - + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); authorLoginPage.loginAs(author.getLogin(), author.getPassword()); UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou"); @@ -342,9 +335,7 @@ public class AssessmentTest { public void scormCourseWithAssessment(@InitialPage LoginPage authorLoginPage, @Drone @User WebDriver ryomouBrowser) throws IOException, URISyntaxException { - //File upload only work with Firefox - Assume.assumeTrue(browser instanceof FirefoxDriver); - + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); authorLoginPage.loginAs(author.getLogin(), author.getPassword()); UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou"); @@ -462,9 +453,7 @@ public class AssessmentTest { public void assessmentMode_manual(@InitialPage LoginPage authorLoginPage, @Drone @Student WebDriver ryomouBrowser, @Drone @Participant WebDriver kanuBrowser) throws IOException, URISyntaxException { - //File upload only work with Firefox - Assume.assumeTrue(browser instanceof FirefoxDriver); - + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); authorLoginPage.loginAs(author.getLogin(), author.getPassword()); UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou"); @@ -878,9 +867,7 @@ public class AssessmentTest { @Drone @User WebDriver ryomouBrowser, @Drone @Participant WebDriver kanuBrowser) throws IOException, URISyntaxException { - //File upload only work with Firefox - Assume.assumeTrue(browser instanceof FirefoxDriver); - + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); UserVO kanu = new UserRestClient(deploymentUrl).createRandomUser("Kanu"); UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou"); @@ -1066,8 +1053,6 @@ public class AssessmentTest { public void taskWithIndividuScoreAndRevision(@InitialPage LoginPage authorLoginPage, @Drone @User WebDriver ryomouBrowser) throws IOException, URISyntaxException { - //File upload only work with Firefox - Assume.assumeTrue(browser instanceof FirefoxDriver); UserVO author = new UserRestClient(deploymentUrl).createAuthor(); UserVO kanu = new UserRestClient(deploymentUrl).createRandomUser("kanu"); diff --git a/src/test/java/org/olat/selenium/ImsQTI21Test.java b/src/test/java/org/olat/selenium/ImsQTI21Test.java index c1e33877c9a..8e0f6d13d5a 100644 --- a/src/test/java/org/olat/selenium/ImsQTI21Test.java +++ b/src/test/java/org/olat/selenium/ImsQTI21Test.java @@ -73,6 +73,340 @@ public class ImsQTI21Test { @Page private NavigationPage navBar; + /** + * Test the flow of the simplest possible test with our + * optimization (jump automatically to the next question, + * jump automatically the close test). The test has one + * part and 2 questions, no feedbacks, no review allowed... + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_noParts_noFeedbacks(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_without_feedbacks.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile) + .clickToolbarRootCrumb(); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + qtiPage + .assertOnAssessmentItem() + .answerSingleChoice("Incorrect response") + .saveAnswer() + .assertOnAssessmentItem("Second question") + .selectItem("First question") + .assertOnAssessmentItem("First question") + .answerSingleChoice("Correct response") + .saveAnswer() + .answerMultipleChoice("Correct response") + .saveAnswer() + .endTest()//auto close because 1 part, no feedbacks + .assertOnAssessmentTestTerminated(); + } + + /** + * Test the flow of a test with questions feedbacks and test + * feedback. + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_noParts_withFeedbacks(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_with_feedbacks.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile) + .clickToolbarRootCrumb(); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + qtiPage + .assertOnAssessmentItem() + .answerSingleChoice("Wrong answer") + .saveAnswer() + .assertFeedback("Oooops") + .answerSingleChoice("Correct answer") + .saveAnswer() + .assertFeedback("Well done") + .nextAnswer() + .assertOnAssessmentItem("Numerical entry") + .answerGapText("69", "_RESPONSE_1") + .saveAnswer() + .assertFeedback("Not really") + .answerGapText("42", "_RESPONSE_1") + .saveAnswer() + .assertFeedback("Ok") + .endTest() + .assertOnAssessmentTestFeedback("All right") + .closeTest() + .assertOnAssessmentTestTerminated(); + } + + /** + * A test with a single part, feedback for questions and + * tests and the resource options "show results at the end + * of the test". + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_noParts_feedbacksAndResults(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_with_feedbacks.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + qtiPage + .clickToolbarBack() + .options() + .showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions()) + .save(); + + qtiPage + .clickToolbarBack() + .assertOnAssessmentItem() + .answerSingleChoice("Wrong answer") + .saveAnswer() + .assertFeedback("Oooops") + .nextAnswer() + .assertOnAssessmentItem("Numerical entry") + .answerGapText("42", "_RESPONSE_1") + .saveAnswer() + .assertFeedback("Ok") + .endTest() + .assertOnAssessmentTestFeedback("Not for the best") + .closeTest() + .assertOnAssessmentTestMaxScore(2) + .assertOnAssessmentTestScore(1) + .assertOnAssessmentTestNotPassed(); + } + + /** + * A test with a single part, feedback for questions and + * tests and the resource options "show results at the end + * of the test". + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_parts_noFeedbacksButResults(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_parts_without_feedbacks.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + qtiPage + .clickToolbarBack() + .options() + .showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions()) + .save(); + + qtiPage + .clickToolbarBack() + .startTestPart() + .selectItem("First question") + .assertOnAssessmentItem("First question") + .answerSingleChoice("Correct") + .saveAnswer() + .assertOnAssessmentItem("Second question") + .answerMultipleChoice("True") + .saveAnswer() + .endTestPart() + .selectItem("Third question") + .assertOnAssessmentItem("Third question") + .answerMultipleChoice("Correct") + .saveAnswer() + .answerCorrectKPrim("True", "Right") + .answerIncorrectKPrim("Wrong", "False") + .saveAnswer() + .endTestPart() + .assertOnAssessmentTestMaxScore(4) + .assertOnAssessmentTestScore(4) + .assertOnAssessmentTestPassed(); + } + + /** + * Test with 2 parts and test feedbacks. + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_parts_feedbacks(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_with_parts_and_test_feedbacks.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile) + .clickToolbarRootCrumb(); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + + qtiPage + .startTestPart() + .selectItem("First question") + .assertOnAssessmentItem("First question") + .answerSingleChoice("Correct answer") + .saveAnswer() + .assertOnAssessmentItem("Second question") + .answerMultipleChoice("Valid answer") + .saveAnswer() + .endTestPart() + .selectItem("Third question") + .assertOnAssessmentItem("Third question") + .answerSingleChoice("Right") + .saveAnswer() + .answerSingleChoice("Good") + .saveAnswer() + .endTestPart() + .assertOnAssessmentTestFeedback("Well done") + .closeTest() + .assertOnAssessmentTestTerminated(); + } + + /** + * Test with time limit. + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_timeLimits(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "Timed QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_time_limits.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile) + .clickToolbarRootCrumb(); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + //check simple time limit + qtiPage + .assertOnAssessmentItem("Single choice") + .answerSingleChoice("Correct answer") + .saveAnswer() + .assertOnAssessmentItem("Last choice") + .answerSingleChoice("True") + .saveAnswer() + .assertOnAssessmentTestTerminated(15); + } + + /** + * Test with time limit and wait for the results at the end. + * + * @param authorLoginPage + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void qti21TestFlow_timeLimits_results(@InitialPage LoginPage authorLoginPage) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createAuthor(); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //upload a test + String qtiTestTitle = "Timed QTI 2.1 " + UUID.randomUUID(); + URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_time_limits.zip"); + File qtiTestFile = new File(qtiTestUrl.toURI()); + navBar + .openAuthoringEnvironment() + .uploadResource(qtiTestTitle, qtiTestFile) + .clickToolbarRootCrumb(); + + QTI21Page qtiPage = QTI21Page + .getQTI12Page(browser); + qtiPage + .options() + .showResults(Boolean.TRUE, new QTI21AssessmentResultsOptions(true, true, false, false, false, false)) + .save(); + + //check simple time limit + qtiPage + .clickToolbarBack() + .assertOnAssessmentItem("Single choice") + .answerSingleChoice("Correct answer") + .saveAnswer() + .assertOnAssessmentItem("Last choice") + .answerSingleChoice("True") + .saveAnswer() + .assertOnAssessmentResults(15) + .assertOnAssessmentTestPassed() + .assertOnAssessmentTestMaxScore(2) + .assertOnAssessmentTestScore(2); + } + /** * Upload a test in QTI 2.1 format, create a course, bind * the test in a course element, run it and check if @@ -84,7 +418,7 @@ public class ImsQTI21Test { */ @Test @RunAsClient - public void qti21Test(@InitialPage LoginPage authorLoginPage) + public void qti21TestInCourse(@InitialPage LoginPage authorLoginPage) throws IOException, URISyntaxException { UserVO author = new UserRestClient(deploymentUrl).createAuthor(); @@ -146,7 +480,6 @@ public class ImsQTI21Test { .answerSingleChoice("Right") .saveAnswer() .endTest() - .closeTest() .assertOnCourseAttempts(1) .assertOnCourseAssessmentTestScore(1); } @@ -233,7 +566,6 @@ public class ImsQTI21Test { .answerSingleChoice("Right") .saveAnswer() .endTest() - .closeTest() .assertOnAssessmentResults() .closeAssessmentResults() .assertOnCourseAttempts(1) @@ -278,8 +610,7 @@ public class ImsQTI21Test { .answerHotspot("circle") .saveAnswer() .assertFeedback("Correct!") - .endTest() - .closeTest(); + .endTest(); //check the results qtiPage .assertOnAssessmentResults() @@ -355,8 +686,7 @@ public class ImsQTI21Test { .answerCorrectKPrim("Deutschland", "Uruguay") .answerIncorrectKPrim("Frankreich", "Spanien") .saveAnswer() - .endTest() - .closeTest(); + .endTest(); //check the results qtiPage diff --git a/src/test/java/org/olat/selenium/UserTest.java b/src/test/java/org/olat/selenium/UserTest.java index 128900bb7e5..ed244bacfbe 100644 --- a/src/test/java/org/olat/selenium/UserTest.java +++ b/src/test/java/org/olat/selenium/UserTest.java @@ -34,7 +34,6 @@ import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; -import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; import org.olat.restapi.support.vo.CourseVO; @@ -51,8 +50,8 @@ import org.olat.selenium.page.user.PortalPage; import org.olat.selenium.page.user.UserAdminPage; import org.olat.selenium.page.user.UserPasswordPage; import org.olat.selenium.page.user.UserPreferencesPageFragment; -import org.olat.selenium.page.user.UserProfilePage; import org.olat.selenium.page.user.UserPreferencesPageFragment.ResumeOption; +import org.olat.selenium.page.user.UserProfilePage; import org.olat.selenium.page.user.UserToolsPage; import org.olat.selenium.page.user.VisitingCardPage; import org.olat.test.ArquillianDeployments; @@ -62,7 +61,6 @@ import org.olat.user.restapi.UserVO; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.firefox.FirefoxDriver; /** * @@ -581,9 +579,6 @@ public class UserTest { @RunAsClient public void browserBack(@InitialPage LoginPage loginPage) throws IOException, URISyntaxException { - - Assume.assumeTrue(browser instanceof FirefoxDriver); - loginPage .loginAs("administrator", "openolat") .resume(); diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java index 2311aeda4b0..110c4d33bb0 100644 --- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java +++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java @@ -69,29 +69,31 @@ public class QTI21Page { return this; } + public QTI21Page startTestPart() { + By startBy = By.xpath("//button[contains(@onclick,'advanceTestPart')]"); + browser.findElement(startBy).click(); + OOGraphene.waitBusy(browser); + By menuBy = By.id("o_qti_menu"); + OOGraphene.waitElement(menuBy, 5, browser); + return this; + } + public QTI21Page assertOnAssessmentItem() { By assessmentItemBy = By.cssSelector("div.qtiworks.o_assessmentitem.o_assessmenttest"); OOGraphene.waitElement(assessmentItemBy, 5, browser); return this; } - //TODO still qti 1.2 - public QTI21Page selectItem(int position) { - By itemsBy = By.cssSelector("a.o_sel_qti_menu_item"); - List<WebElement> itemList = browser.findElements(itemsBy); - Assert.assertTrue(itemList.size() > position); - WebElement itemEl = itemList.get(position); - itemEl.click(); - OOGraphene.waitBusy(browser); + public QTI21Page assertOnAssessmentItem(String title) { + By itemTitleBy = By.xpath("//div[@class='o_assessmentitem_wrapper']/h4[contains(normalize-space(.),'" + title + "')]"); + OOGraphene.waitElement(itemTitleBy, 5, browser); return this; } - public QTI21Page answerSingleChoice(int selectPosition) { - By itemsBy = By.cssSelector("div.choiceInteraction input[type='radio']"); - List<WebElement> optionList = browser.findElements(itemsBy); - Assert.assertTrue(optionList.size() > selectPosition); - WebElement optionEl = optionList.get(selectPosition); - optionEl.click(); + public QTI21Page selectItem(String title) { + By itemBy = By.xpath("//div[@id='o_qti_menu']//li[contains(@class,'o_qti_menu_item')]//a[span[contains(normalize-space(.),'" + title + "')]]"); + OOGraphene.waitElement(itemBy, 5, browser); + browser.findElement(itemBy).click(); OOGraphene.waitBusy(browser); return this; } @@ -167,6 +169,10 @@ public class QTI21Page { return this; } + public QTI21Page endTestPart() { + return endTest(); + } + public QTI21Page endTest() { By endBy = By.cssSelector("a.o_sel_end_testpart"); browser.findElement(endBy).click(); @@ -219,6 +225,18 @@ public class QTI21Page { return this; } + /** + * This check specifically if the metadata of the test are visible. + * + * @param timeout + * @return + */ + public QTI21Page assertOnAssessmentResults(int timeout) { + By resultsBy = By.cssSelector("div.o_sel_results_details"); + OOGraphene.waitElement(resultsBy, timeout, browser); + return this; + } + public QTI21Page assertOnCourseAssessmentTestScore(int score) { By resultsBy = By.xpath("//div[contains(@class,'o_personal')]//tr[contains(@class,'o_score')]/td[contains(text(),'" + score + "')]"); OOGraphene.waitElement(resultsBy, 5, browser); @@ -231,12 +249,54 @@ public class QTI21Page { return this; } + public QTI21Page assertOnAssessmentTestPassed() { + By notPassedBy = By.cssSelector("div.o_sel_results_details tr.o_state.o_passed "); + OOGraphene.waitElement(notPassedBy, 5, browser); + return this; + } + + public QTI21Page assertOnAssessmentTestNotPassed() { + By notPassedBy = By.cssSelector("div.o_sel_results_details tr.o_state.o_failed "); + OOGraphene.waitElement(notPassedBy, 5, browser); + return this; + } + public QTI21Page assertOnAssessmentTestMaxScore(int score) { By resultsBy = By.xpath("//div[contains(@class,'o_sel_results_details')]//tr[contains(@class,'o_sel_assessmenttest_maxscore')]/td[contains(text(),'" + score + "')]"); OOGraphene.waitElement(resultsBy, 5, browser); return this; } + public QTI21Page assertOnAssessmentTestFeedback(String feedback) { + By feedbackBy = By.xpath("//div[contains(@class,'o_info')]/h3[contains(text(),'" + feedback + "')]"); + OOGraphene.waitElement(feedbackBy, 5, browser); + List<WebElement> feedbackEls = browser.findElements(feedbackBy); + Assert.assertEquals(1, feedbackEls.size()); + return this; + } + + /** + * Check if the assessment terminated message is visible. + * + * @return Itself + */ + public QTI21Page assertOnAssessmentTestTerminated() { + By terminatedBy = By.cssSelector("div.o_sel_assessment_test_terminated"); + OOGraphene.waitElement(terminatedBy, 5, browser); + return this; + } + + /** + * Check if the assessment terminated message is visible. + * + * @return Itself + */ + public QTI21Page assertOnAssessmentTestTerminated(int timeout) { + By terminatedBy = By.cssSelector("div.o_sel_assessment_test_terminated"); + OOGraphene.waitElement(terminatedBy, timeout, browser); + return this; + } + /** * Yes in a dialog box controller. */ diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip new file mode 100644 index 0000000000000000000000000000000000000000..b234b7db61a3d18481d2d15f980054b6e7a8ccd5 GIT binary patch literal 5996 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 YOj`dTF5c~JyqjkM69dDK=JxHs0KC<PC;$Ke literal 0 HcmV?d00001 diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip b/src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip new file mode 100644 index 0000000000000000000000000000000000000000..539d85c0a4807d87eb6ffcfea977937e428824fe GIT binary patch literal 5406 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 NuO7E%8|}&6KLFn*ivR!s literal 0 HcmV?d00001 diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip new file mode 100644 index 0000000000000000000000000000000000000000..630b9e52df65a0eb1300550173d4ece8921c6bc5 GIT binary patch literal 4401 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 UB0sMI03bU*au)yq++W@O4}Q(&GXMYp literal 0 HcmV?d00001 diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip new file mode 100644 index 0000000000000000000000000000000000000000..4cf9fcd0a1d93f52da699f72d142ec7323dcf448 GIT binary patch literal 6221 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 literal 0 HcmV?d00001 diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip new file mode 100644 index 0000000000000000000000000000000000000000..eb9cd0d4e5b287eff47d9e39fa93593005ac571b GIT binary patch literal 3599 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 literal 0 HcmV?d00001 -- GitLab