Commit 76865a68 authored by Leon Arnott's avatar Leon Arnott
Browse files

IAdded (hooks-named:), as well as the debugging macros (assert:) and (assert-exists:).

(assert-exists:) is designed as something of a compromise over adding a non-nullable hook selector, pleading that such a selector proved too complicated to add in terms of concatenation with nullable selectors, making various non-error-propagating methods suddenly able to propagate errors, etc. When I added this, (assert:) naturally had to follow.

I MAY change (hooks-named:) to just (hooks:), or perhaps add some aliases for it.

Closes #32. Closes #196.

Also:
 * Fixed a bug where (meter:) wouldn't work properly with fractional variables.
 * Added some more block elements to usuallyBlockElements in Utils.
parent e35e035aca69
......@@ -14,7 +14,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* Fixed a long-standing bug where lambdas would produce an incorrect duplicate-name error if the temp variables used with their clauses contained capital letters.
* Fixed a long-standing bug where hooks that had `(transition:)` transitions would restart their transition animations whenever the containing passage finished transitioning in. Previously, the only way to overcome this was to make the passage transition using `(transition-arrive:"instant")`.
* Fixed a long-standing bug where you couldn't use the column markup to create empty columns by placing two column markup lines in succession, without an intervening blank line.
* Fixed a long-standing bug where strings containing HookName syntax (such as `"?pear"`) were considered identical to actual hooknames (such as `?pear`). This was NOT intended behaviour and was not documented, and as such, certain design patterns that involved constructing HookName strings programmatically (such as `(replace:"?" + $name)`) will no longer work.
* Fixed a long-standing bug where strings containing HookName syntax (such as `"?pear"`) were considered identical to actual hooknames (such as `?pear`). This was NOT intended behaviour and was not documented, and as such, certain design patterns that involved constructing HookName strings programmatically (such as `(replace:"?" + $name)`) will no longer work. A new macro, `(hooks-named:)`, has been added (see below) to provide this feature officially.
* Fixed a bug where the default CSS for `(click: ?Page)` (a blue border around the page) wasn't visible. (Now, an `::after` pseudo-element is created for the enchantment, so that the border displays above all the page content.)
* Fixed a bug where typing `is not an` instead of `is not a` (such as in `$wallet is not an array`) would cause an error.
* Now, trying to access the `0th` or `0thlast` value in an array or string produces an error.
......@@ -135,6 +135,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
###### Values
* Added a `(hooks-named:)` macro, which can be used to construct a HookName using a string. The slightly unusual name of this macro is meant to avoid confusion with `(hook:)`.
* Added a `(trunc:)` macro, which is similar to `(round:)`, but rounds toward zero, "truncating" the fractional component. It is named after the Excel function that performs the same rounding.
* Added a `(plural:)` macro, which takes a number and a string, and combines them, automatically pluralising the string if the number isn't 1. `(plural:2,"duck")` produces `"2 ducks"`. Pluralisation is performed by adding "s" - a second string may be given which specifies a more correct plural form of the word (such as `(plural:4,"elf","elves")`).
* Added a `(str-nth:)` macro (alias `(string-nth)`) which takes a number and produces the English ordinal of that number (such as `"2nd"` for `2`, and `"-41st"` for `-41`).
......@@ -211,6 +212,8 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* Added `(icon-reload:)` and `(icon-fullscreen:)`, two additional commands that can be used, with `(prepend:)` and `(append:)`, to add "fullscreen" and "reload" buttons to the sidebar. The "fullscreen" icon works similarly to the `(link-fullscreen:)` macro described above, while the "reload" icon performs similarly to `(reload:)` when clicked. These offer the same options as `(icon-undo:)` and `(icon-redo:)`.
* Added `(icon-counter:)`, which produces a numeric display element that live-updates to match a bound number variable (rounded to a whole number). The element is roughly the same size as the sidebar icons, and designed to be used in the sidebar alongside them.
* Added a debugging command, `(mock-visits:)`, which lets you re-create a game state where certain passages have been visited a certain number of times, so that the `visits` keyword and the `(history:)` array produce desired results. This command will cause an error unless it's used in debug mode.
* Added a debugging command, `(assert:)`, which can be used to write an "assertion" expression about the game state of your story that should always be `true`, such as `(assert: $HP <= $maxHP)`. If some bug causes the assertion to produce `false`, a helpful error will be produced.
* Added a debugging command, `(assert-exists:)`, which checks whether a hook matching the passed-in HookName, or a text occurrence matching the passed-in string, exists in the passage. If not, an error is produced.
##### Custom Macros
......
......@@ -33,6 +33,9 @@ define(['jquery', 'utils', 'utils/renderutils', 'utils/operationutils'], ($, Uti
means that you'll have to be extra careful while typing the hook name, as misspellings will not be easily identified
by Harlowe itself.
If you wish to construct a hook name programmatically, based on an existing string variable, then the (hooks-named:) macro may be
used in place of the above syntax.
Built in hook names:
There are five special built-in hook names, ?Page, ?Passage, ?Sidebar, ?Link and ?Visited, which, in addition to selecting named hooks,
......@@ -382,7 +385,16 @@ define(['jquery', 'utils', 'utils/renderutils', 'utils/operationutils'], ($, Uti
let ret = '';
const {type, data} = this.selector;
if (type === "name") {
ret += "?" + data;
/*
(hooks-named:) can produce HookNames that the standard syntax cannot, such as those with whitespace.
So, serialisation should produce a macro call if the name can't be reduced to the standard syntax.
*/
if (!data.match(RegExp("^" + Utils.anyRealLetter + "+$"))) {
ret += '(hooks-named:' + JSON.stringify(data) + ")";
}
else {
ret += "?" + data;
}
}
else if (type === "string") {
ret += JSON.stringify(data);
......
......@@ -36,6 +36,7 @@ define(['jquery', 'utils'], ($, Utils) => {
+ "I don't understand it either, but it usually means that an expression was badly written.",
propagated: "Click the 'Open' button to see the code hook as it was executed.",
user: "This is a custom error created by (error:). It usually means you used a custom macro incorrectly.",
assertion: "This command exists to provide a helpful error if a certain important condition wasn't true.",
},
/*
......@@ -90,6 +91,10 @@ define(['jquery', 'utils'], ($, Utils) => {
hidden code hook from which the error transpired.
*/
innerDOM,
/*
This is a hack for the benefit of just (assert:)'s errors.
*/
appendTitleText: false,
});
},
......@@ -146,7 +151,9 @@ define(['jquery', 'utils'], ($, Utils) => {
const errorElement = $("<tw-error class='"
+ (this.type === "javascript" ? "javascript ": "")
+ (this.warning ? "warning" : "error")
+ "' title='" + escape(titleText) + "'>" + escape(this.message) + "</tw-error>"),
+ "' title='" + escape(titleText) + "'>" + escape(
this.message + (this.appendTitleText ? " " + titleText : "")
) + "</tw-error>"),
/*
The explanation text element.
*/
......
"use strict";
define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', 'internaltypes/twineerror',
'internaltypes/twinenotifier', 'datatypes/assignmentrequest', 'datatypes/hookset', 'datatypes/codehook', 'datatypes/lambda', 'datatypes/colour', 'datatypes/gradient', 'internaltypes/varref', 'datatypes/typedvar', 'datatypes/varbind', 'utils/operationutils', 'utils/renderutils'],
($, Macros, Utils, State, Passages, Renderer, Engine, TwineError, TwineNotifier, AssignmentRequest, HookSet, CodeHook, Lambda, Colour, Gradient, VarRef, TypedVar, VarBind, {printBuiltinValue, objectName, clone}, {dialog, geomParse, geomStringRegExp}) => {
($, Macros, Utils, State, Passages, Renderer, Engine, TwineError, TwineNotifier, AssignmentRequest, HookSet, CodeHook, Lambda, Colour, Gradient, VarRef, TypedVar, VarBind, {printBuiltinValue, objectName, clone, toSource}, {dialog, geomParse, geomStringRegExp}) => {
/*d:
Command data
......@@ -492,6 +492,10 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
without using (source:). So, you can't provide it to (upperfirst:) and other such macros.
`(upperfirst: (verbatim-print: $name))` will produce an error. Instead, convert the original string
using (upperfirst:) before giving it to (verbatim-print:).
If you have a string you need to print frequently, and you don't want to call (verbatim-print:) every time you need to print it,
you may wish to simply (set:) a (verbatim-print:) into a variable, like so: `(set: $vbName to (verbatim-print:$name))`. Then, you can
put the command (set in that variable) into passage prose, and it will work as expected.
See also:
(verbatim:), (print:)
......@@ -999,7 +1003,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
that variable changes.
Example usage:
* `(set:$batteryPower to 800)(meter: bind $batteryPower, 1000, "X", "Battery Power", (gradient: 90, 0, red, 1, orange))` creates a centered meter showing the value of the $batteryPower variable,
* `(set:$batteryPower to 800)(meter: bind $batteryPower, 1000, "X", "Battery Power: $batteryPower", (gradient: 90, 0, red, 1, orange))` creates a centered meter showing the value of the $batteryPower variable,
from 0 to 1000, using a gradient from orange (full) to red (empty).
* `(set:$threatLevel to 2)(b4r:'solid')(meter: bind $threatLevel, 10, "==X", red)` creates a right-aligned meter showing the value of the $threatLevel variable,
from 0 to 10, in red, with a solid border.
......@@ -1023,6 +1027,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
right-alignment. Centre-alignment causes the bar to fill from the centre, expanding outward in both directions.
The second, optional string is a label that is placed inside the meter, on top of the bar. This text is aligned in the same direction as the meter itself.
Whenever the meter updates, the label is also re-rendered.
Either a colour or a gradient can be given as the final value, with which to colour the bar. If neither is given, the bar will be a simple gray.
If a gradient is given, and it isn't a (stripes:) gradient, its rotation will be automatically changed to 90 degrees, with the leftmost colour
......@@ -1037,7 +1042,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
(icon-counter:)
Added in: 3.2.0.
#interface
#interface 1
*/
Macros.addCommand('meter',
(_, __, widthStr, labelOrGradient) => {
......@@ -1154,7 +1159,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
const value = bind.varRef.get();
if (typeof value === "number") {
const icon = cd.target.children('tw-meter');
icon.attr('style', makeStyleString(value > 0 ? floor(value) : ceil(value)));
icon.attr('style', makeStyleString(value));
if (labelOrGradient) {
/*
Re-render the label, in the same manner as (cycling-link:).
......@@ -1166,10 +1171,6 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
}
};
let value = bind.varRef.get();
/*
This check should have the same logic as the (trunc:) macro.
*/
value = value > 0 ? floor(value) : ceil(value);
/*
Though it's possible for it to be redefined, a type restriction check might as well be performed now
when the command is being created.
......@@ -2635,12 +2636,120 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
Added in: 3.2.0
#debugging 1
*/
("ignore", noop, noop, [zeroOrMore(Everything)]);
("ignore", noop, noop, [zeroOrMore(Everything)])
/*d:
(assert-exists: HookName or String) -> Command
A debugging macro that confirms whether a hook with the given name, or passage text matching the given string, is present in the passage.
If not, it will produce a helpful error. Use this to test whether enchantment macros like (click:), (enchant:) or (show:) are working properly.
Example usage:
* `(assert-exists: "the auroch")` will produce an error if the text "the auroch" isn't present in the passage.
* `(assert-exists: ?bottomBar)` will produce an error if a ?bottomBar hook isn't present in the passage.
Rationale:
The macros in Harlowe that remotely affect other hooks or text based on their name or contents, such as (click:), are designed such that they
do not cause an error if no matching hooks or text is found in the passage. This allows them to be thought of as similar to CSS rules for how passage prose
is to be rendered - something like `(enchant:?dust, (text-style:'blur'))` states the "rule" that ?dust hooks are to be blurred - rather than as imperative commands
that must be fulfilled there and then. This means that they can be placed in every passage, via "header" or "footer" tagged passages, without errors occurring.
But, this flexibility comes at a cost. In the minority of situations where you need to be certain that a macro is affecting a visible in-passage
structure, you'll often want to test with this macro, so as to produce an error if those structures do not exist.
Details:
This command (and note that (assert:) doesn't produce a command) probes the current passage in which it's located in order to determine whether to produce an error or not.
As such, like all commands, it can be saved into a variable, and deployed into passage code using that variable, to save having to retype it in full.
If you provide an empty string to this macro (which obviously can't be found in the passage), it will produce a different kind of error than what would be desired.
See also:
(error:), (assert:)
Added in: 3.2.0
#debugging 5
*/
("assert-exists",
selector => {
/*
The only error that should be produced at command-creation time is that of basic type-checking.
*/
if (selector === "") {
return TwineError.create('datatype', "(assert-exists:) mustn't be given an empty string.");
}
},
(cd, section, selector) => {
let count = 0;
/*
Rather than call .hooks(), and have to clean up the <tw-pseudo-hook> elements afterward, we simply call .forEach().
*/
(typeof selector === "string" ? HookSet.create({type: "string", data: selector}) : selector).forEach(section, () => { ++count; });
if (!count) {
return TwineError.create("assertion",
`I didn't see any ${typeof selector === "string" ? "text occurrences of" : "hooks matching"} ${toSource(selector)} in this passage.`
);
}
return cd;
},
[either(HookSet, String)]);
/*
The following couple of macros are not commands, but they are each intrinsically related to some of the macros above.
*/
Macros.add
/*d:
(assert: Boolean) -> Instant
A debugging macro that produces a helpful error whenever the expression given to it produces Boolean false. Use this when testing your story
to help ensure that certain facts about the game state are true or not.
Example usage:
`(assert: $isWounded is $isBleeding or $isBandaged)` ensures that if $isWounded is true, one of $isBleeding or $isBandaged MUST be true, and if it's false,
both of them MUST be false. Otherwise, an error is produced.
Rationale:
Harlowe's debug mode provides panels to check the current game state, the contents of variables, active enchantments, and so forth, providing assistance
in identifying bugs. Of course, knowing what variables contain is not the same as knowing whether the relationships between them are being maintained, and
a way of encoding these relationships, in your story, can provide an additional layer of security when debugging your game.
(assert:) allows you to *assert* facts about the game state, facts which absolutely must be true, so much so that
an error should be produced. For example, if your story's code assumes that the variable $nails will always be smaller than or equal to $maxNails, and you want to
ensure that no bugs are written that cause $nails to be greater, you may write `(assert: $nails is <= $maxNails)`, and place that call in
a "debug-header" tagged passage. Thus, should a bug ever appear which causes these variables to no longer maintain their relationship, the (assert:) call will produce an error.
Details:
Note that there are other tools within Harlowe to ensure that variables are obeying certain restrictions, which make the need for certain simple (assert:) calls
unnecessary. Chief among these features is TypedVars, which can be provided to (set:) to permanently and automatically restrict a certain variable to a narrow range
of data. Instead of writing `(assert: $petals is an int)`, you can change the earliest (set:) call that creates $petals to `(set: int-type $petals to 0)`.
Though this is classed as a "debugging" macro, this works even outside of debug mode.
This can also be useful within custom macros, as a shortened form of combining (if:) with (error:). However, the error message produced by (assert:) may not
be as insightful as the customisable error message given to (error:), so it's not especially recommended for use within custom macros that are meant for other authors
to use.
See also:
(error:), (assert-exists:)
Added in: 3.2.0
#debugging 4
*/
("assert",
(_, condition) => condition ? ({
TwineScript_TypeID: "instant",
TwineScript_TypeName: "an (assert:) operation",
TwineScript_ObjectName: "an (assert:) operation",
TwineScript_Unstorable: true,
// Being unstorable and not used by any other data strctures, this doesn't need a ToSource() function.
TwineScript_Print: () => false,
}) :
/*
The appendTitleText expando is used exclusively to append the visible error text with its titleText (which is the source of the <tw-expression>
itself, usually). While a more sophisticated protocol could be devised, I currently (Dec 2020) deem it unnecessary.
*/
assign(TwineError.create("assertion", "An assertion failed: "), { appendTitleText:true }),
[Boolean])
/*d:
(save-game: String, [String]) -> Boolean
......
......@@ -338,7 +338,7 @@ define(['utils', 'macros', 'state', 'utils/operationutils', 'datatypes/changerco
attempting to call (error:) outside of a custom macro's CodeHook will cause another (also different from intended) error.
See also:
(output:), (output-data:)
(output:), (output-data:), (assert:)
Added in: 3.2.0
#custom macros 4
......
......@@ -554,7 +554,7 @@ define(['jquery','macros', 'utils', 'utils/renderutils', 'datatypes/colour', 'da
If an empty string is given to this macro, an error will be produced.
See also:
(hidden:)
(hidden:), (hooks-named:)
Added in: 1.0.0
#styling
......@@ -2233,7 +2233,9 @@ define(['jquery','macros', 'utils', 'utils/renderutils', 'datatypes/colour', 'da
(cd, section, hook, transition, transitionTime) => {
hook.forEach(section, target => {
/*
TBW
The "zoom" trasition continues to transition-in the element from the mouse pointer's position, even though
a click may or may not have been used to reveal this (animate:) call. The following is
largely copied from (transition:)'s code, above.
*/
let transitionOrigin;
if (name === "zoom") {
......
"use strict";
define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatypes/gradient', 'datatypes/datatype', 'internaltypes/twineerror'],
(Macros, {realWhitespace, nth, anyRealLetter}, {subset, objectName, clone, toSource}, Colour, Gradient, Datatype, TwineError) => {
define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatypes/gradient', 'datatypes/datatype', 'datatypes/hookset', 'internaltypes/twineerror'],
(Macros, {realWhitespace, nth, anyRealLetter}, {subset, objectName, clone, toSource}, Colour, Gradient, Datatype, HookSet, TwineError) => {
/*
Built-in value macros.
These macros manipulate the primitive values - boolean, string, number.
......@@ -1114,6 +1114,47 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
},
[Number, positiveInteger, Colour, rest(Colour)])
/*d:
(hooks-named: String) -> HookName
When given a string, this creates a HookName from it. This can be used to dynamically create HookNames.
Example usage:
```
|oracle)["I scry with sticks, not bones."]|mage)["No teeth in the jawbones?"]|bodyguard)["Don't sift through rot."]
(set: $companionType to "bodyguard")
(link:"Investigate the bones")[(show:(hooks-named:$companionType))]
```
Rationale:
The standard syntax for referring to hooks, in macros such as (replace:), (change:) or (show:), is to write a HookName, such as `?door`. That syntax,
though, requires that you hard-code the name of the hook. This macro lets you construct a HookName from one or more existing strings or other variables,
so that the exact hook referenced depends on the game state.
This macro is called (hooks-named:) to avoid confusion with (hook:), and also to convey that a HookName will refer to any number of hooks as long as they
have the same name.
Details:
Note that the HookNames produced by this macro have the same functionality as other HookNames. In particular, you can specify the `1st` hook, `2ndlast` and so forth
by writing, for instance, `(hooks-named: "A")'s 2ndlast`. Also note that the built-in HookNames can be constructed with this macro - `(hooks-named:"passage")` is
the same as `?passage`.
If an empty string is given, then this will cause an error.
See also:
(hook:)
Added in: 3.2.0
#basics
*/
("hooks-named", (_, data) => {
if (!data) {
return TwineError.create("datatype", "(hooks-named:) can't be given an empty string.");
}
return HookSet.create({type:"name", data});
}, [String])
/*d:
(cond: Boolean, Any, ...Any) -> Any
......@@ -1197,8 +1238,8 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
/*d:
Boolean data
Computers can perform more than just mathematical tasks - they are also virtuosos in classical logic. Much as how
arithmetic involves manipulating numbers with addition, multiplication and such, logic involves manipulating the
Branching stories involve the player making choices, and the game using its own programmed logic to react to those choices.
Much as how arithmetic involves manipulating numbers with addition, multiplication and such, logic involves manipulating the
values `true` and `false` using its own operators. Those are not text strings - they are values as fundamental as
the natural numbers. In computer science, they are both called *Booleans*, after the 19th century mathematician
George Boole.
......@@ -1229,13 +1270,9 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
| `is not a`, `is not an` | Evaluates to boolean `true` if the right side does not describe the left side. | `"Boo" is not an empty`, `2 is not an odd`
Conditions can quickly become complicated. The best way to keep things straight is to use parentheses to
group things.
group sub-statements and express order of operations.
*/
/*
JS library wrapper macros
*/
/*
Filter out NaN and Infinities, throwing an error instead.
This is only applied to functions that can create non-numerics,
......
......@@ -6,10 +6,11 @@ define(['jquery', 'markup', 'utils/polyfills'],
{fromCharCode} = String,
// These two are used by childrenProbablyInline (see below).
usuallyBlockElements = (
// The most common block HTML tags that would be used in passage source
// The most common block HTML tags that would be used in passage source.
"audio,blockquote,canvas,div,h1,h2,h3,h4,h5,hr,ol,p,pre,table,ul,video,"
// And the one(s) that Harlowe itself creates through its syntax
+ "tw-align,tw-story,tw-passage"
// And the one(s) that Harlowe itself creates through its syntax.
// <tw-consecutive-br> is deliberately not included.
+ "tw-align,tw-story,tw-passage,tw-sidebar,tw-columns,tw-column,tw-meter,tw-dialog"
).split(','),
usuallyInlineElements = (
......@@ -17,7 +18,7 @@ define(['jquery', 'markup', 'utils/polyfills'],
"a,b,i,em,strong,sup,sub,abbr,acronym,s,strike,del,big,small,script,img,button,input,"
// And the ones that Harlowe itself creates through its syntax.
// Note that <tw-hook> and <tw-expression> aren't included.
+ "tw-link,tw-broken-link,tw-verbatim,tw-collapsed,tw-error"
+ "tw-link,tw-broken-link,tw-verbatim,tw-collapsed,tw-error,tw-colour,tw-icon"
).split(','),
// Certain HTML elements cannot have their parents unwrapped: <audio>, for instance,
......
......@@ -530,50 +530,6 @@ describe("basic command macros", function() {
});
});
});
describe("the (mock-visits:) macro", function() {
beforeEach(function() {
var t = "(print:visits)";
createPassage(t, "grault");
createPassage(t, "garply");
createPassage(t, "qux");
});
it("takes any number of passage name strings", function() {
expect("(mock-visits:)").markupToError();
expect("(mock-visits:'bar')").markupToError();
expect("(mock-visits:'bar','baz')").markupToError();
expect("(mock-visits:'qux')").not.markupToError();
expect("(mock-visits:'qux','bar')").markupToError();
expect("(mock-visits:'qux','qux','qux')").not.markupToError();
expect("(mock-visits:'qux','garply','grault','grault')").not.markupToError();
expect("(mock-visits:'qux','garply','grault','foo')").markupToError();
});
// Can't test that it only works in debug mode, unfortunately.
it("alters the 'visits' keyword to mock visits to each of the given passages", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
var p = goToPassage("qux");
expect(p.text()).toBe('4');
p = goToPassage("garply");
expect(p.text()).toBe('1');
p = goToPassage("grault");
expect(p.text()).toBe('2');
});
it("each successive (mock-visits:) call overrides the last", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
var p = goToPassage("qux");
expect(p.text()).toBe('4');
runPassage("(mock-visits:'garply')");
p = goToPassage("garply");
expect(p.text()).toBe('2');
Engine.goBack();
Engine.goBack();
expect($('tw-passage > :last-child').text()).toBe('4');
});
it("alters the (history:) keyword, adding its strings to the start", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
expect('(history:)').markupToPrint('qux,qux,qux,grault,test');
});
});
describe("the (animate:) macro", function() {
it("takes a hook name, a transition name string that isn't 'instant', and an optional number", function() {
expect("(animate:)").markupToError();
......@@ -591,11 +547,11 @@ describe("basic command macros", function() {
});
it("animates the named hooks with the given animation", function() {
var p = runPassage("|a>[foo]|b>[|a>[foo]](animate:?A,'pulse',9s)");
expect(p.find('tw-transition-container[data-t8n="pulse"] > tw-hook[name="a"]').length).toBe(2);
expect(p.find('tw-hook[data-t8n="pulse"][name="a"]').length).toBe(2);
});
it("changes the animation-duration with the optional number", function() {
var p = runPassage("|a>[foo](animate:?A,'pulse',9s)");
expect(p.find('tw-transition-container[data-t8n="pulse"]').css('animation-duration')).toMatch(/^9000ms$|^9s$/);
expect(p.find('[data-t8n="pulse"]').css('animation-duration')).toMatch(/^9000ms$|^9s$/);
});
});
});
describe("debugging macros", function() {
'use strict';
describe("the (mock-visits:) macro", function() {
beforeEach(function() {
var t = "(print:visits)";
createPassage(t, "grault");
createPassage(t, "garply");
createPassage(t, "qux");
});
it("takes any number of passage name strings", function() {
expect("(mock-visits:)").markupToError();
expect("(mock-visits:'bar')").markupToError();
expect("(mock-visits:'bar','baz')").markupToError();
expect("(mock-visits:'qux')").not.markupToError();
expect("(mock-visits:'qux','bar')").markupToError();
expect("(mock-visits:'qux','qux','qux')").not.markupToError();
expect("(mock-visits:'qux','garply','grault','grault')").not.markupToError();
expect("(mock-visits:'qux','garply','grault','foo')").markupToError();
});
// Can't test that it only works in debug mode, unfortunately.
it("alters the 'visits' keyword to mock visits to each of the given passages", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
var p = goToPassage("qux");
expect(p.text()).toBe('4');
p = goToPassage("garply");
expect(p.text()).toBe('1');
p = goToPassage("grault");
expect(p.text()).toBe('2');
});
it("each successive (mock-visits:) call overrides the last", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
var p = goToPassage("qux");
expect(p.text()).toBe('4');
runPassage("(mock-visits:'garply')");
p = goToPassage("garply");
expect(p.text()).toBe('2');
Engine.goBack();
Engine.goBack();
expect($('tw-passage > :last-child').text()).toBe('4');
});
it("alters the (history:) keyword, adding its strings to the start", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
expect('(history:)').markupToPrint('qux,qux,qux,grault,test');
});
});
describe("the (assert:) macro", function() {
it("takes a boolean", function() {
expect("(assert:3<5)").not.markupToError();
expect("(assert:6<8)").not.markupToError();
expect("(assert:)").markupToError();
expect("(assert:25)").markupToError();
});
it("if given false, it produces an error that shows the call's source", function() {
expect(runPassage("(assert:3<1)").find('tw-error').text()).toContain("(assert:3<1)");
expect(runPassage("(assert:45").find('tw-error').text()).not.toContain("(assert:45)");
});
});
describe("the (assert-exists:) macro", function() {
it("takes a string or hook name", function() {
expect("(assert-exists:true)").markupToError();
expect("(assert-exists:)").markupToError();
expect("(assert-exists:25)").markupToError();
expect("(assert-exists:?red)|red>[]").not.markupToError();
expect("(assert-exists:'bess')bess").not.markupToError();
});
it("errors if the given hook does not exist in the passage", function() {
expect("(assert-exists:?red)|red>[]").not.markupToError();
expect("(assert-exists:'bess')bess").not.markupToError();
});
});
});
......@@ -454,7 +454,8 @@ describe("data structure macros", function () {
});
it("serialises commands", function() {
['(cycling-link:bind $foo,"bar","baz")','(click-goto:"qux","test")',
'(enchant:?foo,(transition:"instant")+(transition-delay:20))'
'(enchant:?foo,(transition:"instant")+(transition-delay:20))',
'(show:(hooks-named:"foo baz"))',
].forEach(function(e){ sourceTest(e,e); });
runPassage("(set:$baz to 4)");
sourceTest("(print:$baz)",'(print:4)');
......
......@@ -97,4 +97,14 @@ describe("game state macros", function() {
});
//TODO: More tests
});
describe("the (hooks-named:) macro", function() {
it("takes a non-empty string", function() {
expect("(show:(hooks-named:'2'))").not.markupToError();
expect("(show:(hooks-named:))").markupToError();
expect("(show:(hooks-named:''))").markupToError();
});
it("produces a usable hookname", function() {
expect("|a>[bar](replace:(hooks-named:'a'))[foo]").markupToPrint('foo');
});
});
});
......@@ -405,6 +405,7 @@ beforeEach(function f(done) {
<script src="spec/command_macros/basics.js"></script>
<script src="spec/command_macros/interface.js"></script>
<script src="spec/command_macros/metadata.js"></script>
<script src="spec/command_macros/debugging.js"></script>
<script src="spec/command_macros/saving.js"></script>
<script src="spec/browser/sessions.js"></script>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment