Skip to content

Commit 5401a81

Browse files
committed
iss1757 - More Jest tests and Ascii block PHP tests
1 parent 67f2e74 commit 5401a81

13 files changed

Lines changed: 833 additions & 12 deletions

corsscripts/ascii/filters/markdownitrules.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export default function markdownitrules(mdit, options) {
5151
return rendered;
5252
};
5353

54+
// Trims lines and removes blank lines before rendering.
55+
// Should we be removing blank lines here or in transforms?
5456
function splitBlock(code) {
5557
return code.split(/\r?\n/).map(line => line.trim()).filter(line => line !== '');
5658
}

corsscripts/ascii/markdownitextensions/asciimathblock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Markdown-it block rule plugin intended to be used in the browser only.
1+
// Markdown-it block rule plugin.
22
//
33
// Syntax:
44
// Opening marker: a single backtick, optionally followed by spaces/tabs, at end of line.

corsscripts/ascii/markdownittransforms/findtextindex.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
// Finds the first occurrence of something in needle that is _outside_ any braces,
2-
// unless it also matches something in without.
1+
// Return the index of the first token from `needle` found at top level in `str`
2+
// (outside any (), [], {}, \lbrace/\rbrace nesting). Tokens listed in `without`
3+
// are ignored even if they also appear in `needle`. Returns false if no match.
4+
// Example: findtextindex('a = {b = c}', ['=']) returns 2.
5+
// Example with without: findtextindex('a = b', ['='], ['=']) returns false.
36
export default function findtextindex(str, needle, without = []) {
47
let braceDepth = 0;
58

corsscripts/ascii/stackascii.bundle.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

corsscripts/ascii/stackascii.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export default function init(inputIds, operations, stack_js) {
5050
if (currentop.reset === 'true') {
5151
filterInput = raw;
5252
}
53+
// We are not explicitly resetting thr=e blockCollector here but expecting
54+
// the filter to do so.
5355
const filterOutput = filter(filterInput, blockCollector, currentop);
5456
if (!displayfixed) {
5557
processedOutput = filterOutput;

stack/cas/castext2/blocks/ascii.block.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ public function compile($format, $options): ?MP_Node {
4242
// Define iframe params.
4343
$xpars = [];
4444
$inputs = [];
45-
$xpars['transforms'] = '';
4645

4746
foreach ($this->params as $key => $value) {
4847
if ($key === 'input') {
@@ -248,14 +247,13 @@ public function validate(
248247
$key !== 'height' &&
249248
$key !== 'aspect-ratio' &&
250249
$key !== 'input' &&
251-
$key !== 'hidden' &&
252-
$key !== 'transforms'
250+
$key !== 'hidden'
253251
) {
254252
$err[] = stack_string('stackBlock_ascii_unknown_param', $key);
255253
$valid = false;
256254
if ($valids === null) {
257255
$valids = [
258-
'width', 'height', 'aspect-ratio', 'input', 'hidden', 'transforms'
256+
'width', 'height', 'aspect-ratio', 'input', 'hidden'
259257
];
260258
$err[] = stack_string('stackBlock_ascii_param', [
261259
'param' => implode(', ', $valids),

tests/ascii_block_test.php

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
<?php
2+
// This file is part of Stack - http://stack.maths.ed.ac.uk/
3+
//
4+
// Stack is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Stack is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Stack. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* PHPUnit tests for the [[ascii]] castext block.
19+
* @package qtype_stack
20+
* @copyright 2026 University of Edinburgh.
21+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
22+
*/
23+
24+
namespace qtype_stack;
25+
26+
use castext2_evaluatable;
27+
use qtype_stack_testcase;
28+
use stack_cas_session2;
29+
30+
defined('MOODLE_INTERNAL') || die();
31+
32+
require_once(__DIR__ . '/../locallib.php');
33+
require_once(__DIR__ . '/fixtures/test_base.php');
34+
require_once(__DIR__ . '/../stack/cas/castext2/castext2_evaluatable.class.php');
35+
require_once(__DIR__ . '/../stack/cas/cassession2.class.php');
36+
require_once(__DIR__ . '/../stack/cas/castext2/blocks/iframe.block.php');
37+
require_once(__DIR__ . '/../stack/cas/castext2/blocks/filter.block.php');
38+
require_once(__DIR__ . '/../stack/cas/castext2/blocks/extractor.block.php');
39+
40+
use stack_cas_castext2_iframe;
41+
42+
/**
43+
* Tests for {@link stack_cas_castext2_ascii}.
44+
* @group qtype_stack
45+
* @group qtype_stack_castext_module
46+
*/
47+
final class ascii_block_test extends qtype_stack_testcase {
48+
49+
/**
50+
* Extract all MP_String values from a compiled MP_List.
51+
* @param \MP_List $compiled
52+
* @return array
53+
*/
54+
private function get_string_items(\MP_List $compiled): array {
55+
$strings = [];
56+
foreach ($compiled->items as $item) {
57+
if ($item instanceof \MP_String) {
58+
$strings[] = $item->value;
59+
}
60+
}
61+
return $strings;
62+
}
63+
64+
/**
65+
* @covers \qtype_stack\stack_cas_castext2_ascii
66+
*/
67+
public function test_basic_ascii_block(): void {
68+
stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///');
69+
70+
$raw = '[[ascii input="ans1"]][[/ascii]]';
71+
$expected = '<div style="width:100%;height:400px;" id="stack-iframe-holder-1"></div>';
72+
73+
$at1 = castext2_evaluatable::make_from_source($raw, 'test-case');
74+
$session = new stack_cas_session2([$at1]);
75+
$session->instantiate();
76+
77+
$this->assertEquals($expected, $at1->apply_placeholder_holder($at1->get_rendered()));
78+
}
79+
80+
/**
81+
* @covers \qtype_stack\stack_cas_castext2_ascii
82+
*/
83+
public function test_ascii_block_with_filter_and_extractor_children(): void {
84+
stack_cas_castext2_iframe::register_counter('///IFRAME_COUNT///');
85+
86+
$raw = '[[ascii input="ans1"]]'
87+
. '[[filter type="markdown" transforms="latexwrap,boldfilter"]][[/filter]]'
88+
. '[[extractor targetinput="ans2" type="lastexpr"]][[/extractor]]'
89+
. '[[/ascii]]';
90+
$expected = '<div style="width:100%;height:400px;" id="stack-iframe-holder-1"></div>';
91+
92+
$at1 = castext2_evaluatable::make_from_source($raw, 'test-case');
93+
$session = new stack_cas_session2([$at1]);
94+
$session->instantiate();
95+
96+
$this->assertTrue($at1->get_valid());
97+
$this->assertEquals($expected, $at1->apply_placeholder_holder($at1->get_rendered()));
98+
}
99+
100+
/**
101+
* @covers \qtype_stack\stack_cas_castext2_ascii
102+
*/
103+
public function test_ascii_compile_adds_default_filter_and_input_request(): void {
104+
$block = new \stack_cas_castext2_ascii(['input' => 'ans1'], []);
105+
$compiled = $block->compile(null, []);
106+
107+
$this->assertInstanceOf(\MP_List::class, $compiled);
108+
$this->assertInstanceOf(\MP_String::class, $compiled->items[0]);
109+
$this->assertEquals('iframe', $compiled->items[0]->value);
110+
111+
$this->assertInstanceOf(\MP_String::class, $compiled->items[1]);
112+
$xpars = json_decode($compiled->items[1]->value, true);
113+
$this->assertEquals('100%', $xpars['width']);
114+
$this->assertEquals('400px', $xpars['height']);
115+
$this->assertStringContainsString('STACK ASCII', $xpars['title']);
116+
117+
$strings = $this->get_string_items($compiled);
118+
$joined = implode("\n", $strings);
119+
$this->assertStringContainsString('stack_js.request_access_to_input("ans1",true)', $joined);
120+
$expectedlinkcode = '{init(inputIds,[{"operation":"filter","type":"markdown","transforms":"latexwrap,boldfilter"}],stack_js);}';
121+
$this->assertStringContainsString($expectedlinkcode, $joined);
122+
$this->assertStringContainsString(
123+
'id="asciiContainerRow" style="width:calc(100% - 20px);height:calc(400px - 30px);"',
124+
$joined
125+
);
126+
}
127+
128+
/**
129+
* @covers \qtype_stack\stack_cas_castext2_ascii
130+
*/
131+
public function test_ascii_compile_uses_child_filter_and_extractor_operations(): void {
132+
$filter = new \stack_cas_castext2_filter([
133+
'type' => 'markdown',
134+
'transforms' => 'latexwrap',
135+
'display' => 'true',
136+
]);
137+
$extractor = new \stack_cas_castext2_extractor([
138+
'type' => 'lastexpr',
139+
'targetinput' => 'ans2',
140+
]);
141+
142+
$block = new \stack_cas_castext2_ascii(
143+
['input' => 'ans1', 'width' => '80%', 'height' => '300px'],
144+
[$filter, $extractor]
145+
);
146+
$compiled = $block->compile(null, []);
147+
148+
$this->assertInstanceOf(\MP_List::class, $compiled);
149+
$xpars = json_decode($compiled->items[1]->value, true);
150+
$this->assertEquals('80%', $xpars['width']);
151+
$this->assertEquals('300px', $xpars['height']);
152+
153+
$strings = $this->get_string_items($compiled);
154+
$joined = implode("\n", $strings);
155+
$this->assertStringContainsString('stack_js.request_access_to_input("ans1",true)', $joined);
156+
$this->assertStringContainsString('stack_js.request_access_to_input("ans2")', $joined);
157+
$expectedlinkcode = '{init(inputIds,[{"type":"markdown","transforms":"latexwrap","display":"true","operation":"filter"}' .
158+
',{"type":"lastexpr","targetinput":"ans2","operation":"extractor"}],stack_js);}';
159+
$this->assertStringContainsString($expectedlinkcode, $joined);
160+
$this->assertStringContainsString(
161+
'id="asciiContainerRow" style="width:calc(80% - 20px);height:calc(300px - 30px);"',
162+
$joined
163+
);
164+
$this->assertStringNotContainsString('"transforms":"latexwrap,boldfilter"', $joined);
165+
}
166+
167+
/**
168+
* @covers \qtype_stack\stack_cas_castext2_ascii
169+
*/
170+
public function test_ascii_requires_input_param(): void {
171+
$raw = '[[ascii]][[/ascii]]';
172+
173+
$at1 = castext2_evaluatable::make_from_source($raw, 'test-case');
174+
175+
$this->assertFalse($at1->get_valid());
176+
$this->assertEquals(stack_string('stackBlock_ascii_input_required'), $at1->get_errors());
177+
}
178+
179+
/**
180+
* @covers \qtype_stack\stack_cas_castext2_ascii
181+
*/
182+
public function test_ascii_validate_width_unit_and_number(): void {
183+
$valid = '[[ascii input="ans1" width="500px"]][[/ascii]]';
184+
$invalidunit = '[[ascii input="ans1" width="500bad"]][[/ascii]]';
185+
$invalidnum = '[[ascii input="ans1" width="-5px"]][[/ascii]]';
186+
187+
$atvalid = castext2_evaluatable::make_from_source($valid, 'test-case');
188+
$this->assertTrue($atvalid->get_valid());
189+
190+
$atunit = castext2_evaluatable::make_from_source($invalidunit, 'test-case');
191+
$session = new stack_cas_session2([$atunit]);
192+
$this->assertFalse($atunit->get_valid());
193+
$this->assertEquals(stack_string('stackBlock_ascii_width'), $atunit->get_errors());
194+
195+
$atnum = castext2_evaluatable::make_from_source($invalidnum, 'test-case');
196+
$session = new stack_cas_session2([$atnum]);
197+
$this->assertFalse($atnum->get_valid());
198+
$this->assertEquals(stack_string('stackBlock_ascii_width_num'), $atnum->get_errors());
199+
}
200+
201+
/**
202+
* @covers \qtype_stack\stack_cas_castext2_ascii
203+
*/
204+
public function test_ascii_validate_height_unit_and_number(): void {
205+
$valid = '[[ascii input="ans1" height="500px"]][[/ascii]]';
206+
$invalidunit = '[[ascii input="ans1" height="500bad"]][[/ascii]]';
207+
$invalidnum = '[[ascii input="ans1" height="-5px"]][[/ascii]]';
208+
209+
$atvalid = castext2_evaluatable::make_from_source($valid, 'test-case');
210+
$this->assertTrue($atvalid->get_valid());
211+
212+
$atunit = castext2_evaluatable::make_from_source($invalidunit, 'test-case');
213+
$session = new stack_cas_session2([$atunit]);
214+
$this->assertFalse($atunit->get_valid());
215+
$this->assertEquals(stack_string('stackBlock_ascii_height'), $atunit->get_errors());
216+
217+
$atnum = castext2_evaluatable::make_from_source($invalidnum, 'test-case');
218+
$session = new stack_cas_session2([$atnum]);
219+
$this->assertFalse($atnum->get_valid());
220+
$this->assertEquals(stack_string('stackBlock_ascii_height_num'), $atnum->get_errors());
221+
}
222+
223+
/**
224+
* @covers \qtype_stack\stack_cas_castext2_ascii
225+
*/
226+
public function test_ascii_aspect_ratio_dimension_rules(): void {
227+
$overdefined = '[[ascii input="ans1" width="100%" height="400px" aspect-ratio="1"]][[/ascii]]';
228+
$underdefined = '[[ascii input="ans1" aspect-ratio="1"]][[/ascii]]';
229+
230+
$atover = castext2_evaluatable::make_from_source($overdefined, 'test-case');
231+
$this->assertFalse($atover->get_valid());
232+
$this->assertEquals(stack_string('stackBlock_ascii_overdefined_dimension'), $atover->get_errors());
233+
234+
$atunder = castext2_evaluatable::make_from_source($underdefined, 'test-case');
235+
$this->assertFalse($atunder->get_valid());
236+
$this->assertEquals(stack_string('stackBlock_ascii_underdefined_dimension'), $atunder->get_errors());
237+
}
238+
239+
/**
240+
* @covers \qtype_stack\stack_cas_castext2_ascii
241+
*/
242+
public function test_ascii_unknown_param_rejected(): void {
243+
$raw = '[[ascii input="ans1" bad_param="x"]][[/ascii]]';
244+
245+
$at1 = castext2_evaluatable::make_from_source($raw, 'test-case');
246+
247+
$this->assertFalse($at1->get_valid());
248+
$this->assertStringContainsString(stack_string('stackBlock_ascii_unknown_param', 'bad_param'), $at1->get_errors());
249+
$this->assertStringContainsString(stack_string('stackBlock_ascii_param', [
250+
'param' => 'width, height, aspect-ratio, input, hidden',
251+
]), $at1->get_errors());
252+
}
253+
}

tests/jest/ascii.filters.markdown.test.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ describe('markdown filter', () => {
5454
expect(capturedState).not.toBeNull();
5555
expect(capturedState.transforms).toEqual(['latexwrap', 'boldfilter']);
5656
expect(capturedState.collector).toBe(collector);
57-
expect(Object.keys(capturedState.transformLib).sort()).toEqual(['boldfilter', 'latexwrap']);
5857
});
5958

6059
test('resets shared state to empty transforms and null collector', () => {
@@ -71,9 +70,14 @@ describe('markdown filter', () => {
7170
const markdownModule = require('../../corsscripts/ascii/filters/markdown.js');
7271
const isolatedMarkdown = markdownModule.default || markdownModule;
7372

74-
isolatedMarkdown('plain', { blocks: [] }, { transforms: 'latexwrap' });
75-
isolatedMarkdown('plain', null, { transforms: '' });
73+
const collector = { blocks: [] };
74+
75+
isolatedMarkdown('plain', collector, { transforms: 'latexwrap' });
76+
expect(capturedState).not.toBeNull();
77+
expect(capturedState.transforms).toEqual(['latexwrap']);
78+
expect(capturedState.collector).toBe(collector);
7679

80+
isolatedMarkdown('plain', null, { transforms: '' });
7781
expect(capturedState).not.toBeNull();
7882
expect(capturedState.transforms).toEqual([]);
7983
expect(capturedState.collector).toBeNull();

0 commit comments

Comments
 (0)