1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-12-27 10:30:28 +00:00

Add <$testcase> widget (#7817)

* Initial Commit

* Add note to preview build

* Fix whitespace and indenting

Thanks @pmario

* Fix crash with unset $tiddler attribute on <$data> widget

Thanks @CodaCodr

* Don't duplicate "description" field in test cases

* Use different background colours for nested testcase widgets

* Extend the testcase widget to run tests

* Add testcases to control panel

* Add a view template body template to render testcase tiddlers

* Test edition should display testcases

* Whitespace fixes

* Make testcase tiddler tempalte link to itself

* Styling tweaks

* Docs improvements

* Styling tweaks

* Run the new tw5.com testcases in the test edition

* Update data widget to display its content in JSON

* Add testcase convenience procedure

* Clearer testcases for data widget, and docs tweaks

* Don't expect our intentionally failing test to pass

* Extend testcase default template so that the display format can be chosen

It is selected by setting the variable "displayFormat"

* DataWidget docs typo

* Fix data widget not refreshing

* Links in testcase output switch to the tab containing that tiddler

Thanks to @btheado for the suggestion

* Docs update for 648855e8a5

* Wording tweak

* Add support for narrative tiddlers in test cases

* Documentation improvements

* Cleanup comments

* Remove obsolete code comments

* Simplify template

* Docs update

* Rename $:/core/ui/testcases/DefaultTemplate/SourceTabs from $:/core/ui/testcases/DefaultTemplate/Source

* Use the view template body for failing tests

* Don't reference the geospatial plugin

* "Test case" should be two words

* Fix handling of currentTiddler variable

Fixes problem reported by @btheado in https://github.com/Jermolene/TiddlyWiki5/pull/7817#issuecomment-2103704468

* Prepare for merging
This commit is contained in:
Jeremy Ruston 2024-05-20 11:30:30 +01:00 committed by GitHub
parent f1299120a6
commit ece8b0ee01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1109 additions and 131 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: Only 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: Test case accent colour with no nesting
testcase-accent-level-2: Test case accent colour with 2nd level nesting
testcase-accent-level-3: Test case 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 dynamically represent one or more tiddlers
\*/
(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) {
// It would be expensive to calculate whether the changedTiddlers impact the filter
// identified by the $filter attribute so we just refresh ourselves unconditionally
this.refreshSelf();
return true;
};
exports.data = DataWidget;
})();

View File

@ -0,0 +1,160 @@
/*\
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 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);
this.setVariable("currentTiddler",this.testcaseTestOutput);
}
// 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,64 @@
title: $:/core/ui/testcases/DefaultTemplate
\whitespace trim
\procedure linkcatcherActions()
<%if [<navigateTo>has[title]] %>
<$qualify title=<<state>> name="qualifiedState">
<$action-setfield $tiddler=<<qualifiedState>> text=<<navigateTo>>/>
</$qualify>
<%endif%>
\end
<$let
state={{{ [<qualify "$:/state/testcase">] }}}
>
<div class="tc-test-case-wrapper">
<div class="tc-test-case-header">
<h2>
<$genesis $type={{{ [<linkTarget>!match[]then[$link]else[div]] }}}>
<%if [<testResult>!match[]] %>
<span class={{{ tc-test-case-result-icon [<testResult>!match[fail]then[tc-test-case-result-icon-pass]] [<testResult>match[fail]then[tc-test-case-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 [[Narrative]is[tiddler]] %>
<div class="tc-test-case-narrative">
<$transclude $tiddler="Narrative" mode="block"/>
</div>
<%endif%>
<%if [<testResult>match[fail]] %>
<div class="tc-test-case-result-fail">
<div class="tc-test-case-result-fail-header">
TEST FAILED
</div>
<div class="tc-test-case-result-fail-body">
<$diff-text source=<<expectedHTML>> dest=<<outputHTML>>/>
</div>
</div>
<%endif%>
<div class="tc-test-case-panes">
<div class="tc-test-case-source">
<$macrocall $name="tabs" tabsList="[all[tiddlers]sort[]] -[prefix<state>] -Description -Narrative -ExpectedResult -Output Output +[putfirst[]] -[has[plugin-type]]" state=<<state>> default="Output" template="$:/core/ui/testcases/DefaultTemplate/SourceTabs"/>
</div>
<div class="tc-test-case-divider">
</div>
<div class="tc-test-case-output">
<%if [<displayFormat>!match[]else[wikitext]match[plaintext]] %>
<pre><$view tiddler="Output" format="plainwikified" mode="block"/></pre>
<%else%>
<$linkcatcher actions=<<linkcatcherActions>>>
<$transclude $tiddler="Output" $mode="block"/>
</$linkcatcher>
<%endif%>
</div>
</div>
</div>
</$let>

View File

@ -0,0 +1,24 @@
title: $:/core/ui/testcases/DefaultTemplate/SourceTabs
\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]] [tag[$:/tags/wiki-test-spec-failing]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

@ -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

@ -0,0 +1,31 @@
title: CompoundTiddlers
modified: 20240507221902644
created: 20240507221902644
tags: Concepts
Compound tiddlers are a special type of tiddler that can store one or more payload tiddlers. The tiddlers within a compound tiddler are only accessible via special operations, typically with the TestCaseWidget.
The compound tiddler format is extremely simple, and includes the notable flaw that it does not permit tiddlers that contain a plus sign (`+`) on a line by itself. It is not intended as a general purpose way of storing tiddler data.
Compound tiddlers are identified by having their type field set to `text/vnd.tiddlywiki-multiple`.
The content of a compound tiddler consists of a sequence of tiddlers separated by a plus sign (`+`) on a line by itself. Each tiddler uses the same format as [[.tid files|TiddlerFiles]].
For example:
```
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,27 @@
title: TestCaseTiddlers
modified: 20240507221902644
created: 20240507221902644
tags: Concepts
Test case tiddlers encapsulate one or more tiddlers that can be displayed as a [[test case|TestCaseWidget]]: an independent embedded wiki that can be used for testing or learning purposes.
Test case tiddlers are formatted as CompoundTiddlers, allowing them to contain multiple tiddlers packed into one.
Test case tiddlers have the following fields:
|!Field |!Description |
|<<.field type>> | Should be set to `text/vnd.tiddlywiki-multiple` |
|<<.field tags>> | Test cases are tagged [[$:/tags/wiki-test-spec]]. Test cases that intentionally fail are tagged [[$:/tags/wiki-test-spec-failing]] |
|<<.field description>> |Descriptive heading for the test, intended to make it easy to identify the test |
|<<.field display-format>> |Optional, defaults to `wikitext`. Set to `plaintext` to cause the output to be rended as plain text |
Test case tiddlers with the appropriate tag are shown in $:/ControlPanel
Some payload tiddlers are set aside for special purposes:
|!Tiddler |!Description |
|''Narrative'' |Narrative description of the test, intended to explain the purpose and operation of the test |
|''Output'' |The tiddler that produces the test output |
|''~ExpectedResult'' |HTML of expected result of rendering the ''Output'' tiddler |

View File

@ -5,7 +5,7 @@ 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''
!!.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,33 @@
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: Narrative
Using the data widget to import a tiddler stored in a compound tiddler
+
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,49 @@
title: TestCases/DataWidget/ImportedFilter
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Imported filter definition
display-format: plaintext
title: Narrative
Using the data widget to create copies of all the tiddlers with the title prefix "Day: T", adding the field "custom" set to "Beta"
+
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,29 @@
title: TestCases/DataWidget/ImportedTiddler
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Imported tiddler definition
display-format: plaintext
title: Narrative
Using the data widget to create a tiddler that is a copy of the tiddler "Hello" with the addition of the field "custom" set to "Alpha"
+
title: Output
<$data $tiddler="Hello" custom="Alpha"/>
+
title: Hello
modifier: JoeBloggs
This is the Hello tiddler
+
title: ExpectedResult
<p>[
{
"title": "Hello",
"modifier": "JoeBloggs",
"text": "This is the Hello tiddler",
"custom": "Alpha"
}
]</p>

View File

@ -0,0 +1,30 @@
title: TestCases/DataWidget/Refreshing
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Refreshing the data widget
display-format: plaintext
title: Narrative
Verifying that the JSON output of the data widget is correctly refreshed when the data changes
+
title: Output
<$data title="Epsilon" text={{Subject}}/>
+
title: Subject
Nothing
+
title: Actions
<$action-setfield $tiddler="Subject" text="Theta"/>
+
title: ExpectedResult
<p>[
{
"title": "Epsilon",
"text": "Theta"
}
]</p>

View File

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

View File

@ -0,0 +1,15 @@
title: TestCases/TestCaseWidget/FailingTest
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec-failing]]
description: An example of a failing test
title: Narrative
This test case intentionally fails to show how failures are displayed.
+
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,16 @@
description: currentTiddler should be properly set
tags: $:/tags/wiki-test-spec
title: TestCases/TestCaseTiddler/currentTiddler
type: text/vnd.tiddlywiki-multiple
title: Narrative
currentTiddler variable in Output tiddler should be "Output"
+
title: Output
<$text text=<<currentTiddler>>>
+
title: ExpectedResult
<p>Output</p>

View File

@ -0,0 +1,23 @@
title: TestCases/TranscludeWidget/SimpleTransclusion
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
description: Simple transclusion
title: Narrative
This test case demonstrates transclusion of and links to other tiddlers.
+
title: Output
Good morning, my [[name|Name]] is {{Name}} and I [[live in|Address]] {{Address}}
+
title: Name
Robert Rabbit
+
title: Address
14 Carrot Street, Vegetabletown
+
title: ExpectedResult
text: <p>Good morning, my <a class="tc-tiddlylink tc-tiddlylink-resolves" href="#Name">name</a> is Robert Rabbit and I <a class="tc-tiddlylink tc-tiddlylink-resolves" href="#Address">live in</a> 14 Carrot Street, Vegetabletown</p>

View File

@ -0,0 +1,42 @@
caption: data
created: 20240507221902644
modified: 20240507221902644
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.
<<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 $.
The attribute <<.attr $tiddler>> is used to ingest a single tiddler from the wiki containing the data widget:
<<testcase "TestCases/DataWidget/ImportedTiddler">>
The attribute <<.attr $filter>> is used to ingest multiple tiddlers from the wiki containing the data widget:
<<testcase "TestCases/DataWidget/ImportedFilter">>
! Compound Tiddlers
[[Compound tiddlers|CompoundTiddlers]] 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">>

View File

@ -0,0 +1,101 @@
caption: testcase
created: 20240507221902644
modified: 20240507221902644
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.
The <<.wid testcase>> widget can be used directly as documented below, but it is generally easier to create [[TestCaseTiddlers]]. These are special CompoundTiddlers that can contain multiple payload tiddlers making up a test case.
!! Features
Here is an example of a test case showing the default split view with the source tiddlers on the left and the tiddler titled `Output` rendered on the right.
<<testcase "TestCases/TranscludeWidget/SimpleTransclusion">>
Notice also that clicking on links within the output pane will switch to the tab containing that tiddler.
The text of the payload tiddlers listed 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 test case 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">>
! 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 test case
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 test case.
|!Attribute |!Description |
|<<.attr template>> |Optional title of the template used to display the test case (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 test case template assigns special meanings to a number of payload tiddlers:
|!Tiddler |!Description |
|''Description'' |Descriptive heading for the test, intended to make it easy to identify the test |
|''Narrative'' |Narrative description of the test, intended to explain the purpose and operation of the test |
|''Output'' |The tiddler that produces the test output |
|''~ExpectedResult'' |HTML of expected result of rendering the ''Output'' tiddler |
The test case wiki will inherit variables that are visible to the <<.wid testcase>> widget itself. The default template uses several variables that can be set by the user:
|!Variable |!Description |
|<<.var linkTarget>> |Causes the test case 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.
! Test Czase 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 test case 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") |
! Example
Here is an example of setting up a test case that includes expected test results:
<$testcase>
<$data title="Description" text="Example of a test case 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

@ -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,152 @@ span.tc-translink > a:first-child {
fill: <<colour network-activity-foreground>>;
}
/*
** Test Cases
*/
.tc-test-case-wrapper {
border: 1px solid <<colour foreground>>;
background-color: <<colour muted-foreground>>;
border-radius: 6px;
}
.tc-tiddler-frame .tc-test-case-wrapper {
margin-left: -0.5em;
margin-right: -0.5em;
}
.tc-test-case-wrapper {
background-color: <<colour testcase-accent-level-1>>;
}
.tc-test-case-wrapper .tc-test-case-wrapper {
background-color: <<colour testcase-accent-level-2>>;
}
.tc-test-case-wrapper .tc-test-case-wrapper .tc-test-case-wrapper {
background-color: <<colour testcase-accent-level-3>>;
}
.tc-test-case-header {
font-weight: normal;
margin: 0.5em 0;
padding: 0 0.5em;
}
.tc-test-case-divider {
x-background-color: <<colour muted-foreground>>;
}
.tc-test-case-result-icon {
fill: #fff;
padding: 0.25em;
display: inline-block;
line-height: 0;
border-radius: 1em;
vertical-align: bottom;
margin-right: 0.25em;
}
.tc-test-case-result-icon-pass {
background-color: green;
}
.tc-test-case-result-icon-fail {
background-color: red;
}
.tc-test-case-result-icon svg {
width: 0.5em;
height: 0.5em;
}
.tc-test-case-header > h2,
.tc-test-case-source > pre {
margin: 0;
}
.tc-test-case-header > h2 a.tc-tiddlylink-missing {
font-style: normal;
}
.tc-test-case-result-fail {
border: 1px solid <<colour foreground>>;
background-color: <<colour background>>;
border-radius: 4px;
margin: 0 0.5em;
padding: 0;
}
.tc-test-case-result-fail-header {
background: <<colour diff-delete-background>>;
color: <<colour diff-delete-foreground>>;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 4px;
}
.tc-test-case-result-fail-body {
padding: 4px;
}
.tc-test-case-source > pre {
height: 100%;
}
.tc-test-case-narrative {
padding: 0.5em;
}
.tc-test-case-panes {
display: flex;
align-items: stretch;
flex-wrap: wrap;
padding: 0.5em;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.tc-test-case-source {
flex: 1 0 49%;
min-width: 250px;
}
.tc-test-case-source .tc-tab-content {
background: <<colour background>>;
margin: 0;
}
.tc-test-case-source .tc-field-table {
width: 100%;
}
.tc-test-case-source table.tc-field-table {
margin: 0;
}
.tc-tiddler-frame .tc-edit-texteditor {
margin: 0;
}
.tc-test-case-divider {
flex: 0 0 1.5%;
}
.tc-test-case-source .tc-tab-buttons {
padding-top: 0;
}
.tc-test-case-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 1em;
}
/*
** Flexbox utility classes
*/