diff --git a/src/qtism/common/collections/AbstractCollection.php b/src/qtism/common/collections/AbstractCollection.php
index 2c3c57ccd..d17354245 100644
--- a/src/qtism/common/collections/AbstractCollection.php
+++ b/src/qtism/common/collections/AbstractCollection.php
@@ -96,6 +96,11 @@ public function count(): int
return count($this->dataPlaceHolder);
}
+ public function isEmpty(): bool
+ {
+ return empty($this->dataPlaceHolder);
+ }
+
/**
* Return the current element of the collection while iterating.
*
diff --git a/src/qtism/data/AssessmentSection.php b/src/qtism/data/AssessmentSection.php
index 58f0f59da..698be5585 100644
--- a/src/qtism/data/AssessmentSection.php
+++ b/src/qtism/data/AssessmentSection.php
@@ -288,6 +288,15 @@ public function getSectionParts(): SectionPartCollection
*/
public function setSectionParts(SectionPartCollection $sectionParts): void
{
+ if (!$sectionParts->isEmpty()) {
+ /** @var SectionPart $sectionPart */
+ foreach ($sectionParts as $sectionPart) {
+ $sectionPart->setParent($this);
+ }
+
+ $sectionPart->setIsLast(true);
+ }
+
$this->sectionParts = $sectionParts;
}
@@ -320,4 +329,11 @@ public function getComponents(): QtiComponentCollection
return new QtiComponentCollection($comp);
}
+
+ public function isLastSectionPart(SectionPart $sectionPart): bool
+ {
+ $sectionParts = $this->getSectionParts()->getArrayCopy();
+
+ return end($sectionParts) === $sectionPart;
+ }
}
diff --git a/src/qtism/data/SectionPart.php b/src/qtism/data/SectionPart.php
index c146c58d0..298cfecd0 100644
--- a/src/qtism/data/SectionPart.php
+++ b/src/qtism/data/SectionPart.php
@@ -117,6 +117,10 @@ class SectionPart extends QtiComponent implements QtiIdentifiable, Shufflable
*/
private $timeLimits = null;
+ private ?SectionPart $parent = null;
+
+ private bool $isLast = false;
+
/**
* Create a new instance of SectionPart.
*
@@ -363,4 +367,24 @@ public function __clone()
// Reset observers.
$this->setObservers(new SplObjectStorage());
}
+
+ public function getParent(): ?SectionPart
+ {
+ return $this->parent;
+ }
+
+ public function setParent(SectionPart $parent): void
+ {
+ $this->parent = $parent;
+ }
+
+ public function isLast(): bool
+ {
+ return $this->isLast;
+ }
+
+ public function setIsLast(bool $isLast): void
+ {
+ $this->isLast = $isLast;
+ }
}
diff --git a/src/qtism/data/TestPart.php b/src/qtism/data/TestPart.php
index b9e7a01bf..5453db2d9 100644
--- a/src/qtism/data/TestPart.php
+++ b/src/qtism/data/TestPart.php
@@ -348,20 +348,24 @@ public function getAssessmentSections(): SectionPartCollection
*/
public function setAssessmentSections(SectionPartCollection $assessmentSections): void
{
- if (count($assessmentSections) > 0) {
- // Check that we have only AssessmentSection and/ord AssessmentSectionRef objects.
- foreach ($assessmentSections as $assessmentSection) {
- if (!$assessmentSection instanceof AssessmentSection && !$assessmentSection instanceof AssessmentSectionRef) {
- $msg = 'A TestPart contain only contain AssessmentSection or AssessmentSectionRef objects.';
- throw new InvalidArgumentException($msg);
- }
- }
+ if ($assessmentSections->isEmpty()) {
+ throw new InvalidArgumentException('A TestPart must contain at least one AssessmentSection.');
+ }
- $this->assessmentSections = $assessmentSections;
- } else {
- $msg = 'A TestPart must contain at least one AssessmentSection.';
- throw new InvalidArgumentException($msg);
+ foreach ($assessmentSections as $assessmentSection) {
+ if (
+ !$assessmentSection instanceof AssessmentSection
+ && !$assessmentSection instanceof AssessmentSectionRef
+ ) {
+ throw new InvalidArgumentException(
+ 'A TestPart contain only contain AssessmentSection or AssessmentSectionRef objects.'
+ );
+ }
}
+
+ $assessmentSection->setIsLast(true);
+
+ $this->assessmentSections = $assessmentSections;
}
/**
@@ -419,4 +423,11 @@ public function __clone()
{
$this->setObservers(new SplObjectStorage());
}
+
+ public function isLastSection(AssessmentSection $assessmentSection): bool
+ {
+ $sections = $this->getAssessmentSections()->getArrayCopy();
+
+ return end($sections) === $assessmentSection;
+ }
}
diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php
index f4ebb03c7..1c0012431 100644
--- a/src/qtism/runtime/tests/AbstractSessionManager.php
+++ b/src/qtism/runtime/tests/AbstractSessionManager.php
@@ -31,6 +31,7 @@
use qtism\data\IAssessmentItem;
use qtism\data\NavigationMode;
use qtism\data\SubmissionMode;
+use qtism\data\TestPart;
/**
* The AbstractSessionManager class is a bed for instantiating
@@ -159,6 +160,7 @@ protected function createRoute(AssessmentTest $test): Route
{
$routeStack = [];
+ /** @var TestPart $testPart */
foreach ($test->getTestParts() as $testPart) {
$assessmentSectionStack = [];
@@ -203,16 +205,6 @@ protected function createRoute(AssessmentTest $test): Route
$route->appendRoute($r);
}
- // Add to the last item of the selection the branch rules of the AssessmentSection/testPart
- // on which the selection is applied... Only if the route contains something (empty assessmentSection edge case).
- if ($route->count() > 0) {
- $route->getLastRouteItem()->addBranchRules($current->getBranchRules());
-
- // Do the same as for branch rules for pre conditions, except that they must be
- // attached on the first item of the route.
- $route->getFirstRouteItem()->addPreConditions($current->getPreConditions());
- }
-
array_push($routeStack, $route);
array_pop($assessmentSectionStack);
} elseif ($current instanceof AssessmentItemRef) {
@@ -227,6 +219,8 @@ protected function createRoute(AssessmentTest $test): Route
$finalRoutes = $routeStack;
$route = new SelectableRoute();
+
+ /** @var SelectableRoute $finalRoute */
foreach ($finalRoutes as $finalRoute) {
$route->appendRoute($finalRoute);
}
diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php
index 84283c389..8085ca1ab 100644
--- a/src/qtism/runtime/tests/AssessmentTestSession.php
+++ b/src/qtism/runtime/tests/AssessmentTestSession.php
@@ -43,6 +43,8 @@
use qtism\data\IAssessmentItem;
use qtism\data\NavigationMode;
use qtism\data\processing\ResponseProcessing;
+use qtism\data\rules\BranchRule;
+use qtism\data\rules\PreConditionCollection;
use qtism\data\ShowHide;
use qtism\data\state\Weight;
use qtism\data\storage\php\PhpStorageException;
@@ -2406,10 +2408,12 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions
$stop = false;
while ($route->valid() === true && $stop === false) {
+ $branchRules = $route->current()->getEffectiveBranchRules();
+ $numberOfBranchRules = $branchRules->count();
+
// Branchings?
- if ($ignoreBranchings === false && count($route->current()->getBranchRules()) > 0 && $this->mustApplyBranchRules() === true) {
- $branchRules = $route->current()->getBranchRules();
- for ($i = 0; $i < count($branchRules); $i++) {
+ if ($ignoreBranchings === false && $numberOfBranchRules > 0 && $this->mustApplyBranchRules() === true) {
+ for ($i = 0; $i < $numberOfBranchRules; $i++) {
$engine = new ExpressionEngine($branchRules[$i]->getExpression(), $this);
$condition = $engine->process();
if ($condition !== null && $condition->getValue() === true) {
@@ -2438,20 +2442,7 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions
}
// Preconditions on target?
- if ($ignorePreConditions === false && $route->valid() === true && ($preConditions = $route->current()->getPreConditions()) && count($preConditions) > 0 && $this->mustApplyPreConditions() === true) {
- for ($i = 0; $i < count($preConditions); $i++) {
- $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this);
- $condition = $engine->process();
-
- if ($condition !== null && $condition->getValue() === true) {
- // The item must be presented.
- $stop = true;
- break;
- }
- }
- } else {
- $stop = true;
- }
+ $stop = !($ignorePreConditions === false) || $this->routeMatchesPreconditions($route);
// After a first iteration, we will not performed branching again, as they are executed
// as soon as we leave an item. Chains of branch rules are not expected.
@@ -2465,6 +2456,47 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions
}
}
+ public function routeMatchesPreconditions(Route $route = null): bool
+ {
+ $route = $route ?? $this->getRoute();
+
+ if (!$route->valid()) {
+ return true;
+ }
+
+ $routeItem = $route->current();
+ $testPart = $routeItem->getTestPart();
+ $navigationMode = $testPart->getNavigationMode();
+
+ if ($navigationMode === NavigationMode::LINEAR || $this->mustForcePreconditions()) {
+ return $this->preConditionsMatch($routeItem->getEffectivePreConditions());
+ }
+
+ if ($navigationMode === NavigationMode::NONLINEAR) {
+ return $this->preConditionsMatch($testPart->getPreConditions());
+ }
+
+ return true;
+ }
+
+ private function preConditionsMatch(PreConditionCollection $preConditions): bool
+ {
+ if ($preConditions->count() === 0) {
+ return true;
+ }
+
+ for ($i = 0; $i < $preConditions->count(); $i++) {
+ $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this);
+ $condition = $engine->process();
+
+ if ($condition === null || $condition->getValue() === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* Set the position in the Route at the very next TestPart in the Route sequence or, if the current
* testPart is the last one of the test session, the test session ends gracefully. If the submission mode
@@ -2483,8 +2515,21 @@ public function moveNextTestPart(): void
$route = $this->getRoute();
$from = $route->current();
+ $branchRules = $from->getTestPart()->getBranchRules();
while ($route->valid() === true && $route->current()->getTestPart() === $from->getTestPart()) {
+ /** @var BranchRule $branchRule */
+ foreach ($branchRules as $branchRule) {
+ $engine = new ExpressionEngine($branchRule->getExpression(), $this);
+ $condition = $engine->process();
+
+ if ($condition !== null && $condition->getValue() === true) {
+ $route->branch($branchRule->getTarget());
+
+ break 2;
+ }
+ }
+
$route->next();
}
@@ -3053,7 +3098,7 @@ public function isNextRouteItemPredictible(): bool
}
// Case 4. The next item has preconditions.
- if ($this->mustApplyPreConditions(true) && count($this->getRoute()->getNext()->getPreConditions()) > 0) {
+ if ($this->mustApplyPreConditions(true)) {
return false;
}
@@ -3133,7 +3178,24 @@ protected function mustApplyBranchRules(): bool
*/
protected function mustApplyPreConditions($nextRouteItem = false): bool
{
- $routeItem = ($nextRouteItem === false) ? $this->getCurrentRouteItem() : $this->getRoute()->getNext();
- return ($routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR || $this->mustForcePreconditions() === true);
+ if ($this->mustForcePreconditions()) {
+ return true;
+ }
+
+ $routeItem = $nextRouteItem === false ? $this->getCurrentRouteItem() : $this->getRoute()->getNext();
+
+ if (!$routeItem instanceof RouteItem) {
+ return false;
+ }
+
+ $testPart = $routeItem->getTestPart();
+ $navigationMode = $testPart->getNavigationMode();
+
+ if ($navigationMode === NavigationMode::LINEAR) {
+ return $routeItem->getEffectivePreConditions()->count() > 0;
+ }
+
+ // Now NonLinear Test part pre-conditions must be considered
+ return $navigationMode === NavigationMode::NONLINEAR && $testPart->getPreConditions()->count() > 0;
}
}
diff --git a/src/qtism/runtime/tests/Route.php b/src/qtism/runtime/tests/Route.php
index 6263cb33b..e2f36f71b 100644
--- a/src/qtism/runtime/tests/Route.php
+++ b/src/qtism/runtime/tests/Route.php
@@ -1117,10 +1117,11 @@ public function branch($identifier): void
if ($targetRouteItems[$occurence]->getTestPart() !== $this->current()->getTestPart()) {
// From IMS QTI:
- // In case of an item or section, the target must refer to an item or section
- // in the same testPart [...]
- $msg = 'Branchings to items outside of the current testPart is forbidden by the QTI 2.1 specification.';
- throw new OutOfBoundsException($msg);
+ // In the case of an item or section, the target must refer to an item or section in the same test-part
+ // that has not yet been presented.
+ $this->next();
+
+ return;
}
$this->setPosition($this->getRouteItemPosition($targetRouteItems[$occurence]));
@@ -1133,10 +1134,11 @@ public function branch($identifier): void
if (isset($assessmentSectionIdentifierMap[$id])) {
if ($assessmentSectionIdentifierMap[$id][0]->getTestPart() !== $this->current()->getTestPart()) {
// From IMS QTI:
- // In case of an item or section, the target must refer to an item or section
- // in the same testPart [...]
- $msg = 'Branchings to assessmentSections outside of the current testPart is forbidden by the QTI 2.1 specification.';
- throw new OutOfBoundsException($msg);
+ // In the case of an item or section, the target must refer to an item or section in the same test-part
+ // that has not yet been presented.
+ $this->next();
+
+ return;
}
// We branch to the first RouteItem belonging to the section.
@@ -1148,14 +1150,6 @@ public function branch($identifier): void
// Check for a testPart.
$testPartIdentifierMap = $this->getTestPartIdentifierMap();
if (isset($testPartIdentifierMap[$id])) {
- // We branch to the first RouteItem belonging to the testPart.
- if ($testPartIdentifierMap[$id][0]->getTestPart() === $this->current()->getTestPart()) {
- // From IMS QTI:
- // For testParts, the target must refer to another testPart.
- $msg = 'Cannot branch to the same testPart.';
- throw new OutOfBoundsException($msg);
- }
-
// We branch to the first RouteItem belonging to the testPart.
$this->setPosition($this->getRouteItemPosition($testPartIdentifierMap[$id][0]));
diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php
index 444d19cbd..c29e9a75b 100644
--- a/src/qtism/runtime/tests/RouteItem.php
+++ b/src/qtism/runtime/tests/RouteItem.php
@@ -268,6 +268,30 @@ public function getPreConditions(): PreConditionCollection
return $this->preConditions;
}
+ /**
+ * Get the PreConditions that actually need to be applied considering all the parent elements: TestPart and Section
+ *
+ * @return PreConditionCollection A collection of PreCondition objects.
+ */
+ public function getEffectivePreConditions(): PreConditionCollection
+ {
+ $routeItemPreConditions = new PreConditionCollection([]);
+
+ foreach ($this->getTestPart()->getPreConditions() as $preCondition) {
+ $routeItemPreConditions->attach($preCondition);
+ }
+
+ foreach ($this->getAssessmentSection()->getPreConditions() as $preCondition) {
+ $routeItemPreConditions->attach($preCondition);
+ }
+
+ foreach ($this->getPreConditions() as $preCondition) {
+ $routeItemPreConditions->attach($preCondition);
+ }
+
+ return $routeItemPreConditions;
+ }
+
/**
* Set the PreCondition objects to be applied prior to the RouteItem.
*
@@ -426,4 +450,34 @@ public function getTimeLimits($excludeItem = false): RouteTimeLimitsCollection
return $timeLimits;
}
+
+ public function getEffectiveBranchRules(): BranchRuleCollection
+ {
+ // Checking branching rules at the Item level
+ if (!$this->getBranchRules()->isEmpty()) {
+ return $this->getBranchRules();
+ }
+
+ if (!$this->getAssessmentItemRef()->isLast()) {
+ return new BranchRuleCollection();
+ }
+
+ $parentSection = $this->getAssessmentSection();
+
+ // Checking branching rules at the Section/Subsection level
+ // To get branching rules for the current section, you need to make sure that the current part of the
+ // section is the last part of the current section.
+ do {
+ if (!$parentSection->getBranchRules()->isEmpty()) {
+ return $parentSection->getBranchRules();
+ }
+
+ if (!$parentSection->isLast()) {
+ return new BranchRuleCollection();
+ }
+ } while ($parentSection = $parentSection->getParent());
+
+ // Return branching rules from the Test Part level
+ return $this->getTestPart()->getBranchRules();
+ }
}
diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php
index 7265502f1..9523d1fcb 100644
--- a/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php
+++ b/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php
@@ -248,4 +248,62 @@ public function testBranchingOnPreconditon(): void
$this::assertNull($session['Q03.SCORE']);
$this::assertSame(1.0, $session['Q04.SCORE']->getValue());
}
+
+ public function testBranchingOnTestPartsSimple1(): void
+ {
+ $session = self::instantiate(self::samplesDir() . 'custom/runtime/branchings/branchings_testparts.xml');
+ $session->beginTestSession();
+
+ // We are starting at item Q01, in testPart P01.
+ $this->assertEquals('Q01', $session->getCurrentAssessmentItemRef()->getIdentifier());
+
+ // Let's jump to testPart P03.
+ $session->beginAttempt();
+ $session->endAttempt(new State([new ResponseVariable('RESPONSE', Cardinality::SINGLE, BaseType::IDENTIFIER, new QtiIdentifier('GotoP03'))]));
+ $session->moveNext();
+
+ // We expect to land in testPart P03, item Q03.
+ $this->assertEquals('Q03', $session->getCurrentAssessmentItemRef()->getIdentifier());
+ $this->assertEquals('P03', $session->getCurrentTestPart()->getIdentifier());
+ }
+
+ public function testBranchingRules(): void
+ {
+ $session = self::instantiate(self::samplesDir() . 'custom/runtime/branchings/branching_rules.xml');
+ $session->beginTestSession();
+
+ $this->assertEquals('testPart-1', $session->getCurrentTestPart()->getIdentifier());
+ $this->assertEquals('assessmentSection-1', $session->getCurrentAssessmentSection()->getIdentifier());
+ $this->assertEquals('item-1', $session->getCurrentAssessmentItemRef()->getIdentifier());
+
+ $session->moveNext();
+
+ $this->assertEquals('testPart-1', $session->getCurrentTestPart()->getIdentifier());
+ $this->assertEquals('assessmentSection-1', $session->getCurrentAssessmentSection()->getIdentifier());
+ $this->assertEquals('item-3', $session->getCurrentAssessmentItemRef()->getIdentifier());
+
+ $session->moveNext();
+
+ $this->assertEquals('testPart-1', $session->getCurrentTestPart()->getIdentifier());
+ $this->assertEquals('assessmentSection-3', $session->getCurrentAssessmentSection()->getIdentifier());
+ $this->assertEquals('item-5', $session->getCurrentAssessmentItemRef()->getIdentifier());
+
+ $session->moveNext();
+
+ $this->assertEquals('testPart-3', $session->getCurrentTestPart()->getIdentifier());
+ $this->assertEquals('assessmentSection-5', $session->getCurrentAssessmentSection()->getIdentifier());
+ $this->assertEquals('item-7', $session->getCurrentAssessmentItemRef()->getIdentifier());
+
+ $session->moveNext();
+
+ $this->assertEquals('testPart-4', $session->getCurrentTestPart()->getIdentifier());
+ $this->assertEquals('subsection-1', $session->getCurrentAssessmentSection()->getIdentifier());
+ $this->assertEquals('item-9', $session->getCurrentAssessmentItemRef()->getIdentifier());
+
+ $session->moveNext();
+
+ $this->assertEquals('testPart-5', $session->getCurrentTestPart()->getIdentifier());
+ $this->assertEquals('assessmentSection-8', $session->getCurrentAssessmentSection()->getIdentifier());
+ $this->assertEquals('item-11', $session->getCurrentAssessmentItemRef()->getIdentifier());
+ }
}
diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php
index 9030ba643..31a9af711 100644
--- a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php
+++ b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php
@@ -163,4 +163,35 @@ public function testKillerTestEpicWin(): void
$this::assertFalse($testSession->isRunning());
}
+
+ public function testTestParAndSectionAndItemPreConditionOnLinearTestWorks(): void
+ {
+ $testSession = self::instantiate(self::samplesDir() . 'custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml');
+ $testSession->beginTestSession();
+ $testSession->beginAttempt();
+ $testSession->moveNext();
+
+ // P02, S03, Q04 are skipped due precondition, but Q04.1 is passed
+ $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q04.1');
+
+ $testSession->moveNext();
+
+ // P05 precondition passed
+ $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q05');
+
+ $testSession->moveNext();
+
+ // S06 precondition passed
+ $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q06');
+
+ $testSession->moveNext();
+
+ // S07 precondition passed
+ $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q07');
+
+ $testSession->moveNext();
+
+ // P08 is nonlinear, but it will be skipped, cause pre-conditions apply to non-linear test parts
+ $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q09');
+ }
}
diff --git a/test/qtismtest/runtime/tests/RouteTest.php b/test/qtismtest/runtime/tests/RouteTest.php
index a5b66b178..298f1b779 100644
--- a/test/qtismtest/runtime/tests/RouteTest.php
+++ b/test/qtismtest/runtime/tests/RouteTest.php
@@ -601,18 +601,18 @@ public function testBranchUnknownTarget(): void
public function testBranchToAssessmentItemRefOutsideOfTestPart(): void
{
- $route = self::buildSimpleRoute(Route::class, 2, 1);
- $this->expectException(OutOfBoundsException::class);
- $this->expectExceptionMessage('Branchings to items outside of the current testPart is forbidden by the QTI 2.1 specification.');
- $route->branch('Q2');
+ $route = self::buildSimpleRoute(Route::class, 2, 2);
+ $route->branch('Q3');
+
+ $this->assertEquals('Q2', $route->current()->getAssessmentItemRef()->getIdentifier());
}
public function testBranchToSameTestPart(): void
{
$route = self::buildSimpleRoute(Route::class, 2, 1);
- $this->expectException(OutOfBoundsException::class);
- $this->expectExceptionMessage('Cannot branch to the same testPart.');
$route->branch('T1');
+
+ $this->assertEquals('T1', $route->current()->getTestPart()->getIdentifier());
}
public function testBranchToAnotherTestPart(): void
@@ -655,24 +655,27 @@ public function testBranchToSectionOutsideOfTestPart(): void
$assessmentItemRef1 = new AssessmentItemRef('Q1', 'Q1.xml');
$assessmentItemRef2 = new AssessmentItemRef('Q2', 'Q2.xml');
+ $assessmentItemRef3 = new AssessmentItemRef('Q3', 'Q3.xml');
$assessmentSection1 = new AssessmentSection('S1', 'Section 1', true);
- $assessmentSection1->setSectionParts(new SectionPartCollection([$assessmentItemRef1]));
+ $assessmentSection1->setSectionParts(new SectionPartCollection([$assessmentItemRef1, $assessmentItemRef2]));
$assessmentSection2 = new AssessmentSection('S2', 'Section 2', true);
- $assessmentSection2->setSectionParts(new SectionPartCollection([$assessmentItemRef2]));
+ $assessmentSection2->setSectionParts(new SectionPartCollection([$assessmentItemRef3]));
$testPart1 = new TestPart('T1', new AssessmentSectionCollection([$assessmentSection1]));
$testPart2 = new TestPart('T2', new AssessmentSectionCollection([$assessmentSection2]));
$assessmentTest = new AssessmentTest('Test1', 'Test 1', new TestPartCollection([$testPart1, $testPart2]));
$route->addRouteItem($assessmentItemRef1, $assessmentSection1, $testPart1, $assessmentTest);
- $route->addRouteItem($assessmentItemRef2, $assessmentSection2, $testPart2, $assessmentTest);
-
- $this->expectException(OutOfBoundsException::class);
- $this->expectExceptionMessage('Branchings to assessmentSections outside of the current testPart is forbidden by the QTI 2.1 specification.');
+ $route->addRouteItem($assessmentItemRef2, $assessmentSection1, $testPart1, $assessmentTest);
+ $route->addRouteItem($assessmentItemRef3, $assessmentSection2, $testPart2, $assessmentTest);
$route->branch('S2');
+ $currentRoute = $route->current();
+
+ $this->assertEquals('Q2', $currentRoute->getAssessmentItemRef()->getIdentifier());
+ $this->assertEquals('S1', $currentRoute->getAssessmentSection()->getIdentifier());
}
public function testGetRouteItemPositionUnknownRouteItem(): void
diff --git a/test/samples/custom/runtime/branchings/branching_rules.xml b/test/samples/custom/runtime/branchings/branching_rules.xml
new file mode 100644
index 000000000..cf9e3557e
--- /dev/null
+++ b/test/samples/custom/runtime/branchings/branching_rules.xml
@@ -0,0 +1,103 @@
+
+
+
+
+ true
+
+
+
+
+ true
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/samples/custom/runtime/branchings/branchings_testparts.xml b/test/samples/custom/runtime/branchings/branchings_testparts.xml
new file mode 100644
index 000000000..881cf54d1
--- /dev/null
+++ b/test/samples/custom/runtime/branchings/branchings_testparts.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+ GotoP02
+
+
+
+
+
+
+ GotoP03
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml
new file mode 100644
index 000000000..d18d736b8
--- /dev/null
+++ b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+ true
+
+
+
+ true
+
+
+
+ false
+
+
+
+
+ true
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+