This commit is contained in:
Jeremy Ruston 2024-05-06 21:10:28 +00:00 committed by GitHub
commit 378c910ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1006 additions and 132 deletions

View File

@ -206,6 +206,12 @@ Stylesheets/Caption: Stylesheets
Stylesheets/Expand/Caption: Expand All
Stylesheets/Hint: This is the rendered CSS of the current stylesheet tiddlers tagged with <<tag "$:/tags/Stylesheet">>
Stylesheets/Restore/Caption: Restore
TestCases/Caption: Test Cases
TestCases/Hint: Test cases are self contained examples for testing and learning
TestCases/All/Caption: All Test Cases
TestCases/All/Hint: All Test Cases
TestCases/Failed/Caption: Failed Test Cases
TestCases/Failed/Hint: Just Failed Test Cases
Theme/Caption: Theme
Theme/Prompt: Current theme:
TiddlerFields/Caption: Tiddler Fields

View File

@ -65,6 +65,9 @@ sidebar-tab-foreground-selected: Sidebar tab foreground for selected tabs
sidebar-tab-foreground: Sidebar tab foreground
sidebar-tiddler-link-foreground-hover: Sidebar tiddler link foreground hover
sidebar-tiddler-link-foreground: Sidebar tiddler link foreground
testcase-accent-level-1: Testcase accent colour with no nesting
testcase-accent-level-2: Testcase accent colour with 2nd level nesting
testcase-accent-level-3: Testcase accent colour with 3rd level nesting or higher
site-title-foreground: Site title foreground
static-alert-foreground: Static alert foreground
tab-background-selected: Tab background for selected tabs

View File

@ -0,0 +1,145 @@
/*\
title: $:/core/modules/widgets/data.js
type: application/javascript
module-type: widget
Widget to represent a single item of data
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var DataWidget = function(parseTreeNode,options) {
this.dataWidgetTag = parseTreeNode.type;
this.initialise(parseTreeNode,options);
};
/*
Inherit from the base widget class
*/
DataWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
DataWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
var jsonPayload = JSON.stringify(this.readDataTiddlerValues(),null,4);
var textNode = this.document.createTextNode(jsonPayload);
parent.insertBefore(textNode,nextSibling);
this.domNodes.push(textNode);
};
/*
Compute the internal state of the widget
*/
DataWidget.prototype.execute = function() {
// Construct the child widgets
this.makeChildWidgets();
};
/*
Read the tiddler value(s) from a data widget must be called after the .render() method
*/
DataWidget.prototype.readDataTiddlerValues = function() {
var self = this;
// Start with a blank object
var item = Object.create(null);
// Read any attributes not prefixed with $
$tw.utils.each(this.attributes,function(value,name) {
if(name.charAt(0) !== "$") {
item[name] = value;
}
});
item = new $tw.Tiddler(item);
// Deal with $tiddler, $filter or $compound-tiddler attributes
var tiddlers = [],title;
if(this.hasAttribute("$tiddler")) {
title = this.getAttribute("$tiddler");
if(title) {
var tiddler = this.wiki.getTiddler(title);
if(tiddler) {
tiddlers.push(tiddler);
}
}
}
if(this.hasAttribute("$filter")) {
var filter = this.getAttribute("$filter");
if(filter) {
var titles = this.wiki.filterTiddlers(filter);
$tw.utils.each(titles,function(title) {
var tiddler = self.wiki.getTiddler(title);
tiddlers.push(tiddler);
});
}
}
if(this.hasAttribute("$compound-tiddler")) {
title = this.getAttribute("$compound-tiddler");
if(title) {
tiddlers.push.apply(tiddlers,this.extractCompoundTiddler(title));
}
}
// Convert the literal item to field strings
item = item.getFieldStrings();
if(tiddlers.length === 0) {
if(Object.keys(item).length > 0 && !!item.title) {
return [item];
} else {
return [];
}
} else {
var results = [];
$tw.utils.each(tiddlers,function(tiddler,index) {
var fields = tiddler.getFieldStrings();
results.push($tw.utils.extend({},fields,item));
});
return results;
}
};
/*
Helper to extract tiddlers from text/vnd.tiddlywiki-multiple tiddlers
*/
DataWidget.prototype.extractCompoundTiddler = function(title) {
var tiddler = this.wiki.getTiddler(title);
if(tiddler && tiddler.fields.type === "text/vnd.tiddlywiki-multiple") {
var text = tiddler.fields.text || "",
rawTiddlers = text.split(/\r?\n\+\r?\n/),
tiddlers = [];
$tw.utils.each(rawTiddlers,function(rawTiddler) {
var fields = Object.create(null),
split = rawTiddler.split(/\r?\n\r?\n/mg);
if(split.length >= 1) {
fields = $tw.utils.parseFields(split[0],fields);
}
if(split.length >= 2) {
fields.text = split.slice(1).join("\n\n");
}
tiddlers.push(new $tw.Tiddler(fields));
});
return tiddlers;
} else {
return [];
}
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
DataWidget.prototype.refresh = function(changedTiddlers) {
// Refresh our attributes
var changedAttributes = this.computeAttributes();
// Refresh our children, and indicate that we refreshed if any of our attribute values have changed
return this.refreshChildren(changedTiddlers) || $tw.utils.count(changedAttributes) > 0;
};
exports.data = DataWidget;
})();

View File

@ -0,0 +1,161 @@
/*\
title: $:/core/modules/widgets/testcase.js
type: application/javascript
module-type: widget
Widget to display a test case
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var TestCaseWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
};
/*
Inherit from the base widget class
*/
TestCaseWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
TestCaseWidget.prototype.render = function(parent,nextSibling) {
var self = this;
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
// Create container DOM node
var domNode = this.document.createElement("div");
this.domNodes.push(domNode);
parent.insertBefore(domNode,nextSibling);
// Render the children into a hidden DOM node
var parser = {
tree: [{
type: "widget",
attributes: {},
orderedAttributes: [],
children: this.parseTreeNode.children || []
}]
};
this.contentRoot = this.wiki.makeWidget(parser,{
document: $tw.fakeDocument,
parentWidget: this
});
this.contentContainer = $tw.fakeDocument.createElement("div");
this.contentRoot.render(this.contentContainer,null);
// Create a wiki
this.testcaseWiki = new $tw.Wiki();
// Always load the core plugin
var loadTiddler = function(title) {
var tiddler = self.wiki.getTiddler(title);
if(tiddler) {
self.testcaseWiki.addTiddler(tiddler);
}
}
loadTiddler("$:/core");
loadTiddler("$:/plugins/tiddlywiki/codemirror");
// Load the test case template
// loadTiddler(this.testcaseTemplate);
// Load tiddlers from child data widgets
var tiddlers = [];
this.findChildrenDataWidgets(this.contentRoot.children,"data",function(widget) {
Array.prototype.push.apply(tiddlers,widget.readDataTiddlerValues());
});
var jsonPayload = JSON.stringify(tiddlers);
this.testcaseWiki.addTiddlers(tiddlers);
// Unpack plugin tiddlers
this.testcaseWiki.readPluginInfo();
this.testcaseWiki.registerPluginTiddlers("plugin");
this.testcaseWiki.unpackPluginTiddlers();
this.testcaseWiki.addIndexersToWiki();
// Generate a `transclusion` variable that depends on the values of the payload tiddlers so that the template can easily make unique state tiddlers
this.setVariable("transclusion",$tw.utils.hashString(jsonPayload));
// Generate a `payloadTiddlers` variable that contains the payload in JSON format
this.setVariable("payloadTiddlers",jsonPayload);
// Render the test rendering if required
if(this.testcaseTestOutput && this.testcaseTestExpectedResult) {
var testcaseOutputContainer = $tw.fakeDocument.createElement("div");
var testcaseOutputWidget = this.testcaseWiki.makeTranscludeWidget(this.testcaseTestOutput,{
document: $tw.fakeDocument,
parseAsInline: false,
parentWidget: this,
variables: {
currentTiddler: this.testcaseTestOutput
}
});
testcaseOutputWidget.render(testcaseOutputContainer);
}
// Clear changes queue
this.testcaseWiki.clearTiddlerEventQueue();
// Run the actions if provided
if(this.testcaseWiki.tiddlerExists(this.testcaseTestActions)) {
testcaseOutputWidget.invokeActionString(this.testcaseWiki.getTiddlerText(this.testcaseTestActions));
testcaseOutputWidget.refresh(this.testcaseWiki.changedTiddlers,testcaseOutputContainer);
}
// Set up the test result variables
var testResult = "",
outputHTML = "",
expectedHTML = "";
if(this.testcaseTestOutput && this.testcaseTestExpectedResult) {
outputHTML = testcaseOutputContainer.children[0].innerHTML;
expectedHTML = this.testcaseWiki.getTiddlerText(this.testcaseTestExpectedResult);
if(outputHTML === expectedHTML) {
testResult = "pass";
} else {
testResult = "fail";
}
this.setVariable("outputHTML",outputHTML);
this.setVariable("expectedHTML",expectedHTML);
this.setVariable("testResult",testResult);
}
// Don't display anything if testHideIfPass is "yes" and the tests have passed
if(this.testcaseHideIfPass === "yes" && testResult === "pass") {
return;
}
// Render the page root template of the subwiki
var rootWidget = this.testcaseWiki.makeTranscludeWidget(this.testcaseTemplate,{
document: this.document,
parseAsInline: false,
parentWidget: this
});
rootWidget.render(domNode);
// Trap changes in the wiki and refresh the rendering
this.testcaseWiki.addEventListener("change",function(changes) {
rootWidget.refresh(changes,domNode);
});
};
/*
Compute the internal state of the widget
*/
TestCaseWidget.prototype.execute = function() {
this.testcaseTemplate = this.getAttribute("template","$:/core/ui/testcases/DefaultTemplate");
this.testcaseTestOutput = this.getAttribute("testOutput");
this.testcaseTestActions = this.getAttribute("testActions");
this.testcaseTestExpectedResult = this.getAttribute("testExpectedResult");
this.testcaseHideIfPass = this.getAttribute("testHideIfPass");
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
TestCaseWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if($tw.utils.count(changedAttributes) > 0) {
this.refreshSelf();
return true;
} else {
return this.contentRoot.refresh(changedTiddlers);
}
};
exports["testcase"] = TestCaseWidget;
})();

View File

@ -813,6 +813,21 @@ Widget.prototype.allowActionPropagation = function() {
return true;
};
/*
Find child <$data> widgets recursively. The tag name allows aliased versions of the widget to be found too
*/
Widget.prototype.findChildrenDataWidgets = function(children,tag,callback) {
var self = this;
$tw.utils.each(children,function(child) {
if(child.dataWidgetTag === tag) {
callback(child);
}
if(child.children) {
self.findChildrenDataWidgets(child.children,tag,callback);
}
});
};
/*
Evaluate a variable with parameters. This is a static convenience method that attempts to evaluate a variable as a function, returning an array of strings
*/

View File

@ -95,6 +95,9 @@ table-footer-background: #a8a8a8
table-header-background: #f0f0f0
tag-background: #ec6
tag-foreground: #ffffff
testcase-accent-level-1: #84C5E6
testcase-accent-level-2: #E3B740
testcase-accent-level-3: #5FD564
tiddler-background: <<colour background>>
tiddler-border: <<colour background>>
tiddler-controls-foreground-hover: #888888

View File

@ -0,0 +1,10 @@
title: $:/core/ui/ControlPanel/TestCases
tags: $:/tags/ControlPanel/Advanced
caption: {{$:/language/ControlPanel/TestCases/Caption}}
\whitespace trim
{{$:/language/ControlPanel/TestCases/Hint}}
<div class="tc-control-panel">
<$macrocall $name="tabs" tabsList="[all[shadows+tiddlers]tag[$:/tags/ControlPanel/TestCases]!has[draft.of]]" default="$:/core/ui/ControlPanel/TestCases/All"/>
</div>

View File

@ -0,0 +1,24 @@
title: $:/core/ui/ControlPanel/TestCases/All
tags: $:/tags/ControlPanel/TestCases
caption: {{$:/language/ControlPanel/TestCases/All/Caption}}
\define lingo-base() $:/language/ControlPanel/
<<lingo TestCases/All/Hint>>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/wiki-test-spec]type[text/vnd.tiddlywiki-multiple]] [all[tiddlers+shadows]tag[$:/tags/wiki-test-spec-failing]type[text/vnd.tiddlywiki-multiple]]">
<h2>
<$link>
<$text text=<<currentTiddler>>/>
</$link>
</h2>
<$transclude
$tiddler="$:/core/ui/TestCaseTemplate"
/>
</$list>

View File

@ -0,0 +1,15 @@
title: $:/core/ui/ControlPanel/TestCases/Failed
tags: $:/tags/ControlPanel/TestCases
caption: {{$:/language/ControlPanel/TestCases/Failed/Caption}}
\define lingo-base() $:/language/ControlPanel/
<<lingo TestCases/Failed/Hint>>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/wiki-test-spec]type[text/vnd.tiddlywiki-multiple]] [all[tiddlers+shadows]tag[$:/tags/wiki-test-spec-failing]type[text/vnd.tiddlywiki-multiple]]">
<$transclude
$tiddler="$:/core/ui/TestCaseTemplate"
hideIfPass="yes"
/>
</$list>

View File

@ -0,0 +1,18 @@
title: $:/core/ui/TestCaseTemplate
\parameters (hideIfPass:"no")
\whitespace trim
<$let
linkTarget="yes"
displayFormat={{!!display-format}}
>
<$testcase
testOutput="Output"
testExpectedResult="ExpectedResult"
testActions="Actions"
testHideIfPass=<<hideIfPass>>
>
<$data $compound-tiddler=<<currentTiddler>>/>
<$data title="Description" text={{!!description}}/>
</$testcase>
</$let>

View File

@ -0,0 +1,49 @@
title: $:/core/ui/testcases/DefaultTemplate
\whitespace trim
<$let
state={{{ [<qualify "$:/state/testcase">] }}}
>
<div class="tc-testcase-wrapper">
<div class="tc-testcase-header">
<h2>
<$genesis $type={{{ [<linkTarget>!match[]then[$link]else[div]] }}}>
<%if [<testResult>!match[]] %>
<span class={{{ tc-testcase-result-icon [<testResult>!match[fail]then[tc-testcase-result-icon-pass]] [<testResult>match[fail]then[tc-testcase-result-icon-fail]] +[join[ ]] }}}>
<%if [<testResult>!match[fail]] %>
{{$:/core/images/done-button}}
<%else%>
{{$:/core/images/close-button}}
<%endif%>
</span>
<%endif%>
<$view tiddler="Description" mode="inline"/>
</$genesis>
</h2>
</div>
<%if [<testResult>match[fail]] %>
<div class="tc-testcase-result-fail">
<div class="tc-testcase-result-fail-header">
TEST FAILED
</div>
<div class="tc-testcase-result-fail-body">
<$diff-text source=<<expectedHTML>> dest=<<outputHTML>>/>
</div>
</div>
<%endif%>
<div class="tc-testcase-panes">
<div class="tc-testcase-source">
<$macrocall $name="tabs" tabsList="[all[tiddlers]sort[]] -[prefix<state>] -Description -ExpectedResult -Output Output +[putfirst[]] -[has[plugin-type]]" state=<<state>> default="Output" template="$:/core/ui/testcases/DefaultTemplate/Source"/>
</div>
<div class="tc-testcase-divider">
</div>
<div class="tc-testcase-output">
<%if [<displayFormat>!match[]else[wikitext]match[plaintext]] %>
<pre><$view tiddler="Output" format="plainwikified" mode="block"/></pre>
<%else%>
<$transclude $tiddler="Output" $mode="block"/>
<%endif%>
</div>
</div>
</div>
</$let>

View File

@ -0,0 +1,24 @@
title: $:/core/ui/testcases/DefaultTemplate/Source
\whitespace trim
\procedure body()
<$list filter="[<currentTab>fields[]] -text +[limit[1]]" variable="ignore">
<table class="tc-field-table">
<tbody>
<$list filter="[<currentTab>fields[]sort[]] -text -title title +[putfirst[]]" variable="fieldName">
<tr>
<td>
<$text text=<<fieldName>>/>
</td>
<td>
<$view tiddler=<<currentTab>> field=<<fieldName>>/>
</td>
</tr>
</$list>
</tbody>
</table>
</$list>
<$edit class="tc-edit-texteditor" tiddler=<<currentTab>>/>
\end
<$transclude $variable="body" $mode="inline"/>

View File

@ -0,0 +1,4 @@
title: $:/core/ui/testcases/RawJSONTemplate
\whitespace trim
<$text text=<<payloadTiddlers>>/>

View File

@ -1,6 +1,7 @@
title: $:/config/ViewTemplateBodyFilters/
tags: $:/tags/ViewTemplateBodyFilter
testcase: [tag[$:/tags/wiki-test-spec]type[text/vnd.tiddlywiki-multiple]then[$:/core/ui/TestCaseTemplate]]
stylesheet: [tag[$:/tags/Stylesheet]then[$:/core/ui/ViewTemplate/body/rendered-plain-text]]
core-ui-tags: [tag[$:/tags/PageTemplate]] [tag[$:/tags/EditTemplate]] [tag[$:/tags/ViewTemplate]] [tag[$:/tags/KeyboardShortcut]] [tag[$:/tags/ImportPreview]] [tag[$:/tags/EditPreview]][tag[$:/tags/EditorToolbar]] [tag[$:/tags/Actions]] :then[[$:/core/ui/ViewTemplate/body/code]]
system: [prefix[$:/boot/]] [prefix[$:/config/]] [prefix[$:/core/macros]] [prefix[$:/core/save/]] [prefix[$:/core/templates/]] [prefix[$:/info/]] [prefix[$:/language/]] [prefix[$:/languages/]] [prefix[$:/snippets/]] [prefix[$:/state/]] [prefix[$:/status/]] [prefix[$:/info/]] [prefix[$:/temp/]] +[!is[image]limit[1]then[$:/core/ui/ViewTemplate/body/code]]

View File

@ -0,0 +1,10 @@
title: $:/core/macros/testcase
tags: $:/tags/Macro $:/tags/Global
\whitespace trim
\procedure testcase(tiddler)
<$tiddler tiddler=<<tiddler>>>
<$transclude $tiddler="$:/core/ui/TestCaseTemplate">
</$tiddler>
\end

View File

@ -1,2 +1,2 @@
title: $:/tags/ViewTemplateBodyFilter
list: $:/config/ViewTemplateBodyFilters/hide-body $:/config/ViewTemplateBodyFilters/code-body $:/config/ViewTemplateBodyFilters/stylesheet $:/config/ViewTemplateBodyFilters/core-ui-advanced-search $:/config/ViewTemplateBodyFilters/core-ui-tags $:/config/ViewTemplateBodyFilters/system $:/config/ViewTemplateBodyFilters/import $:/config/ViewTemplateBodyFilters/plugin $:/config/ViewTemplateBodyFilters/default
list: $:/config/ViewTemplateBodyFilters/testcase $:/config/ViewTemplateBodyFilters/hide-body $:/config/ViewTemplateBodyFilters/code-body $:/config/ViewTemplateBodyFilters/stylesheet $:/config/ViewTemplateBodyFilters/core-ui-advanced-search $:/config/ViewTemplateBodyFilters/core-ui-tags $:/config/ViewTemplateBodyFilters/system $:/config/ViewTemplateBodyFilters/import $:/config/ViewTemplateBodyFilters/plugin $:/config/ViewTemplateBodyFilters/default

View File

@ -15,6 +15,7 @@
"tiddlywiki/codemirror",
"tiddlywiki/menubar",
"tiddlywiki/jszip",
"tiddlywiki/innerwiki",
"tiddlywiki/confetti",
"tiddlywiki/dynannotate",
"tiddlywiki/tour"

View File

@ -3,3 +3,7 @@ title: HelloThere
This is TiddlyWiki's browser-based test runner for version <<version>>. See the bottom of this page for the test results.
https://tiddlywiki.com/
! Test Cases
{{$:/core/ui/ControlPanel/TestCases}}

View File

@ -0,0 +1,5 @@
{
"directories": [
"../../../../tw5.com/tiddlers/testcases"
]
}

View File

@ -1,7 +1,8 @@
{
"description": "TiddlyWiki core tests",
"plugins": [
"tiddlywiki/jasmine"
"tiddlywiki/jasmine",
"tiddlywiki/geospatial"
],
"themes": [
"tiddlywiki/vanilla",

View File

@ -0,0 +1,13 @@
title: TestCaseTiddlers
Behind the scenes, the templates used to view TestCaseTiddlers use the <<.wlink TestCaseWidget>> widget.
! Testcase Conventions
The following conventions are used for testcase tiddlers:
* `Description` contains a brief description of the test (rendered in inline mode)
* `Output` contains the tiddler text to be rendered. It can also reference other tiddlers
* `ExpectedResult` contains the HTML that should match the rendering of the tiddler `Output`

View File

@ -5,7 +5,27 @@ tags: TableOfContents
title: HelloThere
type: text/vnd.tiddlywiki
!!.tc-hero-heading ''Welcome to TiddlyWiki, a unique [[non-linear|Philosophy of Tiddlers]] notebook for [[capturing|Creating and editing tiddlers]], [[organising|Structuring TiddlyWiki]] and [[sharing|Sharing your tiddlers with others]] complex information''
<div style="border: 2px solid red; background: #ffd; padding: 0 0.5em; border-radius: 8px;">
This is a preview build of ~TiddlyWiki 5 from the pull request [[#7817: Add <$testcase> widget|https://github.com/Jermolene/TiddlyWiki5/pull/7817]].
It features an experimental new core widget for displaying interactive examples that show the output together with a tabbed display of the constituent tiddlers that produce it.
* TestCaseWidget
* Enhancements to the existing DataWidget
For example:
<$testcase>
<$data title="Description" text="How to calculate 2 plus 2"/>
<$data title="FirstNumber" text="2"/>
<$data title="SecondNumber" text="2"/>
<$data title="Output" text="<$text text={{{ [{FirstNumber}add{SecondNumber}] }}}/>"/>
</$testcase>
</div>
!!.tc-hero-heading ''Welcome to TiddlyWiki, a unique [[non-linear|Philosophy of Tiddlers]] notebook for [[capturing|Creating and editing tiddlers]], [[organising|Structuring TiddlyWiki]] and [[sharing|Sharing your tiddlers with others]] complex information''
Use it to keep your [[to-do list|TaskManagementExample]], to plan an [[essay or novel|"TiddlyWiki for Scholars" by Alberto Molina]], or to organise your wedding. Record every thought that crosses your brain, or build a flexible and responsive website.

View File

@ -0,0 +1,29 @@
title: TestCases/DataWidget/ImportCompound
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Importing a compound payload tiddler and adding custom fields
display-format: plaintext
title: Output
<$data $compound-tiddler="Compound" custom="Alpha"/>
+
title: Compound
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Payload Tiddler
tags: Alpha Beta Gamma
This is a payload tiddler from a compound tiddler
+
title: ExpectedResult
<p>[
{
"title": "Payload Tiddler",
"tags": "Alpha Beta Gamma",
"text": "This is a payload tiddler from a compound tiddler",
"custom": "Alpha"
}
]</p>

View File

@ -0,0 +1,45 @@
title: TestCases/DataWidget/ImportedFilter
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Imported filter definition
display-format: plaintext
title: Output
<$data $filter="[prefix[Day: T]]" custom="Beta"/>
+
title: Day: Monday
text: Today is Monday
+
title: Day: Tuesday
text: Today is Tuesday
+
title: Day: Wednesday
text: Today is Wednesday
+
title: Day: Thursday
text: Today is Thursday
+
title: Day: Friday
text: Today is Friday
+
title: Day: Saturday
text: Today is Saturday
+
title: Day: Sunday
text: Today is Sunday
+
title: ExpectedResult
<p>[
{
"title": "Day: Thursday",
"text": "Today is Thursday",
"custom": "Beta"
},
{
"title": "Day: Tuesday",
"text": "Today is Tuesday",
"custom": "Beta"
}
]</p>

View File

@ -0,0 +1,25 @@
title: TestCases/DataWidget/ImportedTiddler
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Imported tiddler definition
display-format: plaintext
title: Output
<$data $tiddler="HelloThere" custom="Alpha"/>
+
title: HelloThere
modifier: JoeBloggs
This is the HelloThere tiddler
+
title: ExpectedResult
<p>[
{
"title": "HelloThere",
"modifier": "JoeBloggs",
"text": "This is the HelloThere tiddler",
"custom": "Alpha"
}
]</p>

View File

@ -0,0 +1,18 @@
title: TestCases/DataWidget/SimpleTiddler
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Simple tiddler definition
display-format: plaintext
title: Output
<$data title="Epsilon" text="Theta"/>
+
title: ExpectedResult
<p>[
{
"title": "Epsilon",
"text": "Theta"
}
]</p>

View File

@ -0,0 +1,11 @@
title: TestCases/TestCaseWidget/FailingTest
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec-failing]]
description: An example of a failing test
title: Output
The sum is <$text text={{{ [[2]add[2]] }}}/>.
+
title: ExpectedResult
text: <p>The sum is not 8.</p>

View File

@ -0,0 +1,19 @@
title: TestCases/TranscludeWidget/SimpleTransclusion
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Simple transclusion
title: Output
Good morning, my name is {{Name}} and I live in {{Address}}
+
title: Name
Robert Rabbit
+
title: Address
14 Carrot Street, Vegetabletown
+
title: ExpectedResult
text: <p>Good morning, my name is Robert Rabbit and I live in 14 Carrot Street, Vegetabletown</p>

View File

@ -0,0 +1,61 @@
caption: data
created: 20230406161341763
modified: 20230406161341763
tags: Widgets
title: DataWidget
type: text/vnd.tiddlywiki
! Introduction
The data widget is used with the <<.wlink TestCaseWidget>> widget and the [[Innerwiki Plugin]] to specify payload tiddlers that are to be included in the test case or innerwiki.
! Content and Attributes
The content of the data widget is ignored. It supports the following attributes:
|!Attribute |!Description |
|<<.attr $tiddler>> |Optional title of a tiddler to be used as a payload tiddler (optional) |
|<<.attr $filter>> |Optional filter string identifying tiddlers to be used as payload tiddlers (optional) |
|<<.attr $compound-tiddler>> |Optional title of a tiddler containing payload tiddlers in `text/vnd.tiddlywiki-multiple` format (see below) |
|//any attribute<br>not starting<br>with $// |Field values to be assigned to the payload tiddler(s) |
The data widget is not rendered when used within the <<.wlink TestCaseWidget>> widget or the [[Innerwiki Plugin]] but for ease of testing, when used elsewhere it renders a JSON representation of the payload tiddlers.
Without any of the attributes <<.attr $tiddler>>, <<.attr $filter>> or <<.attr $compound-tiddler>>, any attributes whose name does not start with $ are used as the field values for creating a single new tiddler. For example, here a tiddler with the title "Epsilon" and the text "Theta" is created:
<<testcase "TestCases/DataWidget/SimpleTiddler">>
If any of the attributes <<.attr $tiddler>>, <<.attr $filter>> or <<.attr $compound-tiddler>> are specified then they are used to generate base tiddlers that are then modified with the addition of fields derived from any attributes whose name does not start with $.
This example, here we specify a copy of the "HelloThere" tiddler with the addition of the field "custom" set to "Alpha":
<<testcase "TestCases/DataWidget/ImportedTiddler">>
This example injects all image tiddlers with the addition of the field "custom" set to "Beta":
<<testcase "TestCases/DataWidget/ImportedFilter">>
! Compound Tiddlers
Compound tiddlers provide a way to easily create multiple tiddlers from within a single tiddler. They are contained in tiddlers of type `text/vnd.tiddlywiki-multiple`. The text field consists of a series of tiddlers in the same format as `.tid` files, each separated by a line containing a single `+` character.
<<testcase "TestCases/DataWidget/ImportCompound">>
Here is a more complex example of the content of a compound tiddler:
```
title: First
tags: one two
This is the first tiddler
+
title: Second
tags: three four
This is the second tiddler
+
title: third
tags: five six
This is the third tiddler
```

View File

@ -0,0 +1,95 @@
caption: testcase
created: 20230406161341763
modified: 20230406161341763
tags: Widgets
title: TestCaseWidget
type: text/vnd.tiddlywiki
! Introduction
The <<.wid testcase>> widget is designed to present interactive example test cases that are useful for learning and testing. It functions by creating an independent subwiki loaded with the specified payload tiddlers and then rendering a specified template from within the subwiki. The <<.wid testcase>> widget can optionally also be used to run and verify test results within the subwiki.
This makes it possible to run independent tests that also serve as documentation examples.
!! Features
Here is an example of a testcase showing the default split view with the source tiddlers on the left and the tiddler titled `Output` rendered on the right. It also displays the tiddler titled `Description` as the heading.
<<testcase "TestCases/TranscludeWidget/SimpleTransclusion">>
The payload tiddlers listed in the tabs on the left are editable, with the results being immediately reflected in the preview pane on the right. However, if the <<.wid testcase>> widget is refreshed then the modifications are lost.
The green tick at the top left of a testcase indicates that a test has been set up and that it passes.
If the test fails, a red cross is shown, and there is a display of the differences between the actual results and the expected results:
<<testcase "TestCases/TestCaseWidget/FailingTest">>
!! Usage
The <<.wid testcase>> widget can be used directly as documented below, but it is generally easier and more flexible to create [[TestCaseTiddlers]]. These are special, self contained tiddlers that can contain multiple payload tiddlers making up a testcase.
Note that the testcase wiki will inherit variables that are visible to the <<.wid testcase>> widget itself.
! Limitations
The <<.wid testcase>> widget creates a lightweight TiddlyWiki environment that is a parasite of the main wiki. Because it is not a full, independent TiddlyWiki environment, there are some important limitations:
* Output is rendered into a DIV, and so cannot be styled independently of the host wiki
* Any changes to the wiki made interactively by the user are volatile, and are lost when the <<.wid testcase>> widget is refreshed
* Startup actions are not supported
* Only plugins available in the host wiki can be included in the testcase
If these limitations are a problem, the [[Innerwiki Plugin]] offers the ability to embed a fully independent subwiki via an `<iframe>` element, but without the testing related features of the <<.wid testcase>> widget.
! Content and Attributes
The content of the `<$testcase>` widget is not displayed but instead is scanned for <<.wlink DataWidget>> widgets that define the payload tiddlers to be included in the testcase.
|!Attribute |!Description |
|<<.attr template>> |Optional title of the template used to display the testcase (defaults to $:/core/ui/testcases/DefaultTemplate). Note that custom templates will need to be explicitly added to the payload |
|<<.attr testOutput>> |Optional title of the tiddler whose output should be subject to testing (note that both <<.attr testOutput>> and <<.attr testExpectedResult>> must be provided in order for testing to occur) |
|<<.attr testExpectedResult>> |Optional title of the tiddler whose content is the expected result of rendering the output tiddler (note that both <<.attr testOutput>> and <<.attr testExpectedResult>> must be provided in order for testing to occur) |
|<<.attr testActions>> |Optional title of the tiddler containing actions that should be executed before the test occurs |
|<<.attr testHideIfPass>> |If set to "yes", hides the <<.wid testcase>> widget if the test passes |
! Payload Tiddlers
The payload tiddlers are the tiddler values that are loaded into the subwiki that is created to run the tests. They are created via <<.wlink DataWidget>> widgets within the body of the `<$testcase>` widget. The `$:/core` plugin is automatically included in the payload.
! Testcase Templates
The <<.attr template>> attribute defaults to $:/core/ui/testcases/DefaultTemplate
The default template uses several variables that can be set by the user:
|!Variable |!Description |
|<<.var linkTarget>> |Causes the testcase description to be rendered as a link to the current tiddler |
|<<.var displayFormat>> |Defaults to "wikitext", can also be "plaintext" to force plain text display |
A custom template can be specified for special purposes. For example, the provided template $:/core/ui/testcases/RawJSONTemplate just displays the payload tiddlers in JSON, which can be used for debugging purposes.
! Testcase Template Variables
The <<.wid testcase>> widget makes the following variables available within the rendered template:
|!Variable |!Description |
|<<.var transclusion>> |A hash that reflects the names and values of all the payload tiddlers. This makes it easier for testcase templates to create unique state tiddler titles using the [[qualify Macro]] or QualifyWidget |
|<<.var payloadTiddlers>> |JSON array of payload tiddler fields |
|<<.var outputHTML>> |The actual output HTML if running tests |
|<<.var expectedHTML>> |The expected output HTML if running tests |
|<<.var testResult>> |The tests result if running tests (may be "pass" or "fail") |
! Examples
Here is an example of setting up a testcase that includes expected test results:
<$testcase>
<$data title="Description" text="Example of a testcase with expected results"/>
<$data title="Output" text="""<$testcase testOutput="Output" testExpectedResult="ExpectedResult">
<$data title="Description" text="How to calculate 2 plus 2"/>
<$data title="Output" text="<$text text={{{ [[2]add[2]] }}}/>"/>
<$data title="ExpectedResult" text="<p>8</p>"/>
</$testcase>
"""/>
</$testcase>

View File

@ -7,6 +7,7 @@
"tiddlywiki/evernote",
"tiddlywiki/internals",
"tiddlywiki/menubar",
"tiddlywiki/innerwiki",
"tiddlywiki/confetti",
"tiddlywiki/dynannotate",
"tiddlywiki/tour",

View File

@ -0,0 +1,17 @@
/*\
title: $:/plugins/tiddlywiki/innerwiki/anchor.js
type: application/javascript
module-type: widget
Anchor widget to represent an innerwiki graphical anchor. Clone of the data widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.anchor = require("$:/core/modules/widgets/data.js").data;
})();

View File

@ -1,58 +0,0 @@
/*\
title: $:/plugins/tiddlywiki/innerwiki/data.js
type: application/javascript
module-type: widget
Widget to represent a single item of data
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var DataWidget = function(parseTreeNode,options) {
this.dataWidgetTag = parseTreeNode.type;
this.initialise(parseTreeNode,options);
};
/*
Inherit from the base widget class
*/
DataWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
DataWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
this.renderChildren(parent,nextSibling);
};
/*
Compute the internal state of the widget
*/
DataWidget.prototype.execute = function() {
// Construct the child widgets
this.makeChildWidgets();
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
DataWidget.prototype.refresh = function(changedTiddlers) {
// Refresh our attributes
var changedAttributes = this.computeAttributes();
// Refresh our children
return this.refreshChildren(changedTiddlers);
};
exports.data = DataWidget;
exports.anchor = DataWidget;
})();

View File

@ -15,7 +15,7 @@ Widget to display an innerwiki in an iframe
var DEFAULT_INNERWIKI_TEMPLATE = "$:/plugins/tiddlywiki/innerwiki/template";
var Widget = require("$:/core/modules/widgets/widget.js").widget,
DataWidget = require("$:/plugins/tiddlywiki/innerwiki/data.js").data,
DataWidget = require("$:/core/modules/widgets/data.js").data,
dm = $tw.utils.domMaker;
var InnerWikiWidget = function(parseTreeNode,options) {
@ -143,7 +143,7 @@ Create the anchors
*/
InnerWikiWidget.prototype.createAnchors = function() {
var self = this;
this.findDataWidgets(this.children,"anchor",function(widget) {
this.findChildrenDataWidgets(this.children,"anchor",function(widget) {
var anchorWidth = 40,
anchorHeight = 40,
getAnchorCoordinate = function(name) {
@ -233,76 +233,16 @@ InnerWikiWidget.prototype.createInnerHTML = function() {
IMPLANT_PREFIX = "<" + "script>\n$tw.preloadTiddlerArray(",
IMPLANT_SUFFIX = ");\n</" + "script>\n",
parts = html.split(SPLIT_MARKER),
tiddlers = this.readTiddlerDataWidgets(this.children);
tiddlers = [];
this.findChildrenDataWidgets(this.children,"data",function(widget) {
Array.prototype.push.apply(tiddlers,widget.readDataTiddlerValues());
});
if(parts.length === 2) {
html = parts[0] + IMPLANT_PREFIX + JSON.stringify(tiddlers) + IMPLANT_SUFFIX + SPLIT_MARKER + parts[1];
}
return html;
};
/*
Find child data widgets
*/
InnerWikiWidget.prototype.findDataWidgets = function(children,tag,callback) {
var self = this;
$tw.utils.each(children,function(child) {
if(child.dataWidgetTag === tag) {
callback(child);
}
if(child.children) {
self.findDataWidgets(child.children,tag,callback);
}
});
};
/*
Find the child data widgets
*/
InnerWikiWidget.prototype.readTiddlerDataWidgets = function(children) {
var self = this,
results = [];
this.findDataWidgets(children,"data",function(widget) {
Array.prototype.push.apply(results,self.readTiddlerDataWidget(widget));
});
return results;
};
/*
Read the value(s) from a data widget
*/
InnerWikiWidget.prototype.readTiddlerDataWidget = function(dataWidget) {
// Start with a blank object
var item = Object.create(null);
// Read any attributes not prefixed with $
$tw.utils.each(dataWidget.attributes,function(value,name) {
if(name.charAt(0) !== "$") {
item[name] = value;
}
});
// Deal with $tiddler or $filter attributes
var titles;
if(dataWidget.hasAttribute("$tiddler")) {
titles = [dataWidget.getAttribute("$tiddler")];
} else if(dataWidget.hasAttribute("$filter")) {
titles = this.wiki.filterTiddlers(dataWidget.getAttribute("$filter"));
}
if(titles) {
var self = this;
var results = [];
$tw.utils.each(titles,function(title,index) {
var tiddler = self.wiki.getTiddler(title),
fields;
if(tiddler) {
fields = tiddler.getFieldStrings();
}
results.push($tw.utils.extend({},fields,item));
})
return results;
} else {
return [item];
}
};
/*
Compute the internal state of the widget
*/

View File

@ -12,7 +12,7 @@ Tests the wiki based tests
/*global $tw: false */
"use strict";
var TEST_WIKI_TIDDLER_FILTER = "[type[text/vnd.tiddlywiki-multiple]tag[$:/tags/wiki-test-spec]]";
var TEST_WIKI_TIDDLER_FILTER = "[all[tiddlers+shadows]type[text/vnd.tiddlywiki-multiple]tag[$:/tags/wiki-test-spec]]";
var widget = require("$:/core/modules/widgets/widget.js");
@ -24,7 +24,11 @@ describe("Wiki-based tests", function() {
var tiddler = $tw.wiki.getTiddler(title);
it(tiddler.fields.title + ": " + tiddler.fields.description, function() {
// Add our tiddlers
var wiki = new $tw.Wiki();
var wiki = new $tw.Wiki(),
coreTiddler = $tw.wiki.getTiddler("$:/core");
if(coreTiddler) {
wiki.addTiddler(coreTiddler);
}
wiki.addTiddlers(readMultipleTiddlersTiddler(title));
// Complain if we don't have the ouput and expected results
if(!wiki.tiddlerExists("Output")) {

View File

@ -983,7 +983,7 @@ button.tc-btn-invisible.tc-remove-tag-button {
margin-top: {{$:/themes/tiddlywiki/vanilla/metrics/storytop}};
transition: min-height {{$:/config/AnimationDuration}}ms ease-in-out, padding-top {{$:/config/AnimationDuration}}ms ease-in-out, padding-bottom {{$:/config/AnimationDuration}}ms ease-in-out;
}
<<if-no-sidebar """
.tc-sidebar-header {
@ -2264,11 +2264,11 @@ html body.tc-body.tc-single-tiddler-window {
*/
.tc-manager-wrapper {
}
.tc-manager-controls {
}
.tc-manager-control {
@ -3222,6 +3222,141 @@ span.tc-translink > a:first-child {
fill: <<colour network-activity-foreground>>;
}
/*
** Test Cases
*/
.tc-testcase-wrapper {
border: 1px solid <<colour foreground>>;
background-color: <<colour muted-foreground>>;
border-radius: 6px;
}
.tc-testcase-wrapper {
background-color: <<colour testcase-accent-level-1>>;
}
.tc-testcase-wrapper .tc-testcase-wrapper {
background-color: <<colour testcase-accent-level-2>>;
}
.tc-testcase-wrapper .tc-testcase-wrapper .tc-testcase-wrapper {
background-color: <<colour testcase-accent-level-3>>;
}
.tc-testcase-header {
font-weight: normal;
margin: 0.5em 0;
padding: 0 0.5em;
}
.tc-testcase-divider {
x-background-color: <<colour muted-foreground>>;
}
.tc-testcase-result-icon {
fill: #fff;
padding: 0.25em;
display: inline-block;
line-height: 0;
border-radius: 1em;
vertical-align: bottom;
margin-right: 0.25em;
}
.tc-testcase-result-icon-pass {
background-color: green;
}
.tc-testcase-result-icon-fail {
background-color: red;
}
.tc-testcase-result-icon svg {
width: 0.5em;
height: 0.5em;
}
.tc-testcase-header > h2,
.tc-testcase-source > pre {
margin: 0;
}
.tc-testcase-header > h2 a.tc-tiddlylink-missing {
font-style: normal;
}
.tc-testcase-result-fail {
border: 1px solid <<colour foreground>>;
background-color: <<colour background>>;
border-radius: 4px;
margin: 0 0.5em;
padding: 0;
}
.tc-testcase-result-fail-header {
background: <<colour foreground>>;
color: <<colour background>>;
padding: 4px;
}
.tc-testcase-result-fail-body {
padding: 4px;
}
.tc-testcase-source > pre {
height: 100%;
}
.tc-testcase-panes {
display: flex;
align-items: stretch;
flex-wrap: wrap;
padding: 0.5em;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.tc-testcase-source {
flex: 1 0 49%;
min-width: 250px;
}
.tc-testcase-source .tc-tab-content {
background: <<colour background>>;
margin: 0;
}
.tc-testcase-source .tc-field-table {
width: 100%;
}
.tc-testcase-source table.tc-field-table {
margin: 0;
}
.tc-tiddler-frame .tc-edit-texteditor {
margin: 0;
}
.tc-testcase-divider {
flex: 0 0 1.5%;
}
.tc-testcase-source .tc-tab-buttons {
padding-top: 0;
}
.tc-testcase-output {
box-shadow: inset 2px 2px 10px 0px <<colour muted-foreground>>;
background: <<colour background>>;
border-radius: 4px;
border: 1px solid <<colour foreground>>;
flex: 1 0 49%;
min-width: 250px;
padding: 0.25em;
}
/*
** Flexbox utility classes
*/