Jump to content

Module:Template test case: Difference between revisions

From Humanipedia
m 1 revision imported
Line 1: Line 1:
{{WikiProject banner shell|
--[[
{{WikiProject Templates|module_for_template_maintenance=yes}}
  A module for generating test case templates.
}}
{{Central|Template talk:Test case|Template talk:Testcase table|Template talk:Testcase rows|Template talk:Inline test case|Template talk:Collapsible test case|Template talk:Test case nowiki|Template talk:Nowiki template demo|Module talk:Template test case/data|Module talk:Template test case/config}}
{{Centralized archive banner
| {{Archives |root=Template talk:Testcase table |title=Template talk:Testcase table |search=no}}
}}
== Multiple automatic sandbox versions ==
Can we please extend the automatic addition of a <code>/sandbox</code> variant to all <code>_template<var>i</var></code>s if there is no <code>_template<var>++i</var></code>?
— [[User:Crissov|Christoph]] [[User talk:Crissov|Päper]] 11:14, 20 September 2019 (UTC)


== Table with many tests ==
  This module incorporates code from the English Wikipedia's "Testcase table"
For some templates, especially inline templates like {{tl|frac}}, it makes sense to have all or most test cases in a single table with one combination of parameters per row. It would be nice if this could be handled by a single function call by passing multiple parameter sets, e.g.:
   module,[1] written by Frietjes [2] with contributions by Mr. Stradivarius [3]
<syntaxhighlight lang=mediawiki>
   and Jackmcbarn,[4] and the English Wikipedia's "Testcase rows" module,[5]
{{test case|_template1=frac|_template3=sfrac
  written by Mr. Stradivarius.
|_test1={1}      |_label1=single anonymous parameter, numeric
|_test2={1,2}   |_label2=two anonymous parameters, all numeric
|_test3={1,2,3}  |_label3=three anonymous parameters, all numeric
|_test4={1=1}    |_label4=single, first parameter
|_test5={2=1}    |_label5=single, second parameter
|_test6={3=1}   |_label6=single, third parameter
|_test7={1,3=2}  |_label7=first and third parameter
|_test8={2=1,3=2}|_label8=second and third parameter
|_test9={a}      |_label9=single anonymous parameter, alphabetic
|_test10={1,1}  |_label10=two equal parameters
}}
</syntaxhighlight>
11:14, 20 September 2019 (UTC)
: Alternatively, there should be an additional rendering mode ''cells''. It would put the code and each result for all templates and sandbox versions into adjacent cells in a single table row, but the user would have to provide the surrounding table code (but possibly not the rows <code>|-</code>). — [[User:Crissov|Christoph]] [[User talk:Crissov|Päper]] 10:16, 1 January 2020 (UTC)
<syntaxhighlight lang=mediawiki>
{| class="wikitable sortable"
|+ Test cases
|-
! Test case description !! Template call
! {{tl|frac}} !! {{tl|frac/sandbox}} !! {{tl|sfrac}} !! {{tl|sfrac/sandbox}}
|-
{{test case|_format=cells|_template1=frac|_template3=sfrac|1|_label=single anonymous parameter, numeric}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|1|2|_label=two anonymous parameters, all numeric}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|1|2|3|_label=three anonymous parameters, all numeric}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|1=1|_label=single, first parameter}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|2=1|_label=single, second parameter}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|3=1|_label=single, third parameter}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|1|3=2|_label=first and third parameter}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|2=1|3=2|_label=second and third parameter}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|a|_label=single anonymous parameter, alphabetic}}
{{test case|_format=cells|_template1=frac|_template3=sfrac|1|1|_label=two equal parameters}}
|}
</syntaxhighlight>


== Hyphen issue in [[Template:Test case nowiki]] ==
  The "Testcase table" and "Testcase rows" modules are released under the
  CC BY-SA 3.0 License [6] and the GFDL.[7]


This came up when I was working on [[Template:Graphical timeline/testcases]]. At [[Special:Permalink/958740301]] it can be observed that parameter {{para|note3-at}} gets a broken value. First character, hyphen, is correct, however, the first opening brace {{(}} of {{tnull|Period start}} is converted into an [[HTML entity]] for some reason. This results in an error {{tq|<nowiki>Expression error: Unrecognized punctuation character "&"</nowiki>}} somewhere deep inside the template's internals. I've added [[Template:Test_case_nowiki/testcases#Hyphen_prefix_inside_nowiki_tag|a test case to demonstrate the issue on a smaller example]]. —⁠[[User:Andrybak|andrybak]] ([[User talk:Andrybak|talk]]) 13:38, 25 May 2020 (UTC)
  License: CC BY-SA 3.0 and the GFDL
: See [[phab:T168759]] [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 20:45, 25 May 2020 (UTC)
  Author: Mr. Stradivarius


== Something just changed ==
  [1] https://en.wikipedia.org/wiki/Module:Testcase_table
{{Moved|to=Wikipedia:Village_pump_(technical)#Audio_player_doesn't_work_inside_collapsed_templates}}
  [2] https://en.wikipedia.org/wiki/User:Frietjes
From yesterday to today, the boxes at [[Template:Spoken Wikipedia/testcases]] changed from yellow to green, became collapsed, and introduced an error where they now don't seem to be able to embed audio files properly. I'm not sure where the edit that caused this was made, though. Does anyone know? <span style="color:#AAA"><small>&#123;{u&#124;</small><span style="border-radius:9em;padding:0 5px;background:#088">[[User:Sdkb|<span style="color:#FFF">'''Sdkb'''</span>]]</span><small>}&#125;</small></span> <sup>[[User talk:Sdkb|'''talk''']]</sup> 21:20, 9 July 2020 (UTC)
  [3] https://en.wikipedia.org/wiki/User:Mr._Stradivarius
:{{u|Sdkb}}, user {{u|Izno}} has [[Special:Diff/966871510|responded]] to the edit request. That would explain the change from yellow to green on /testcases. Templates {{tl|Spoken Wikipedia}} and {{tl|Spoken Wikipedia/sandbox}} now produce the same output. Audio player is also broken for me. —⁠[[User:Andrybak|andrybak]] ([[User talk:Andrybak|talk]]) 22:13, 9 July 2020 (UTC)
  [4] https://en.wikipedia.org/wiki/User:Jackmcbarn
:I've changed the output of /sandbox, and the audio players are now fixed. Perhaps the player doesn't survive in a collapsed state? —⁠[[User:Andrybak|andrybak]] ([[User talk:Andrybak|talk]]) 22:17, 9 July 2020 (UTC)
  [5] https://en.wikipedia.org/wiki/Module:Testcase_rows
::{{u|Andrybak}}, hmm, interesting. I just checked a few live instances, and it's not causing any problems there. <span style="color:#AAA"><small>&#123;{u&#124;</small><span style="border-radius:9em;padding:0 5px;background:#088">[[User:Sdkb|<span style="color:#FFF">'''Sdkb'''</span>]]</span><small>}&#125;</small></span> <sup>[[User talk:Sdkb|'''talk''']]</sup> 22:18, 9 July 2020 (UTC)
  [6] https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
:::{{ec}}
  [7] https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License
{{od|3}}
]]
{|
|+Tests for media player
|-
! Collapsed !! Not collapsed
|-
|
{{collapsed top|title=Audio player inside}}
[[File:En-Archaea-article.ogg|noicon|200px]]
{{collapsed bottom}}
|
[[File:En-Archaea-article.ogg|noicon|200px]]
|-
|
{{collapsed top|title=Video player inside}}
[[File:Typing example.ogv|noicon|200px]]
{{collapsed bottom}}
|
[[File:Typing example.ogv|noicon|200px]]
|}
Audio player is broken, but video is fine. —⁠[[User:Andrybak|andrybak]] ([[User talk:Andrybak|talk]]) 22:18, 9 July 2020 (UTC)
:{{u|Sdkb}}, I suggest to move this discussion to [[WP:VPT]]. [[Module:Template test case]] is not at fault, it's wider issue. —⁠[[User:Andrybak|andrybak]] ([[User talk:Andrybak|talk]]) 22:23, 9 July 2020 (UTC)


== How to specify page name? ==
-- Load required modules
local yesno = require('Module:Yesno')


I looked through the documentation, but I was unable to find a way to specify the page name (magic word PAGENAME) in a template test case. For templates that use Wikidata calls, for example, the Wikidata call checks the PAGENAME and retrieves the appropriate bit of data from the corresponding Wikidata page. Is there a way to do this? The result would be to render the template as it would appear when processed by [[Special:ExpandTemplates]] with the appropriate PAGENAME entered in the upper text field. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 22:55, 23 July 2020 (UTC)
-- Set constants
:Partial answer, still looking for a real answer: A possible workaround is to use {{para|qid}} in your infobox. See [[Template:Ordination/testcases]] for an example. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 01:23, 3 August 2020 (UTC)
local DATA_MODULE = 'Module:Template test case/data'
: I don't think this is possible. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 02:24, 4 August 2020 (UTC)


== Paragraph break when using {{tl|inline test case}} ==
-------------------------------------------------------------------------------
-- Shared methods
-------------------------------------------------------------------------------


This is probably something silly I am doing, but I can't figure out what. I am using {{tl|inline test case}} in the testcase page of [[Template:Cite certification]] and from some reason I am sometimes (rarely) getting an extra paragraph break between results. I created an example: [[Template:Cite certification/testcases-example]]. I was worried that there is something wrong with the {{tl|Cite certification}}, but as you can see from the code in the example, the paragraph break does not appear when running either the stable or the (identical) sandbox versions. Any ideas? --[[User:Muhandes|Muhandes]] ([[User talk:Muhandes|talk]]) 14:46, 31 December 2020 (UTC)
local function message(self, key, ...)
:There was an invisible character (a [[Left-to-right mark]]) on your testcases example page. I copied the whole page to a text editor, told it to show the invisible characters, and deleted it. [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 16:51, 31 December 2020 (UTC)
-- This method is added to classes that need to deal with messages from the
:: Thanks, I must have wasted five hours of my time finding that one. I now have a good way to test it in the future. --[[User:Muhandes|Muhandes]] ([[User talk:Muhandes|talk]]) 19:36, 31 December 2020 (UTC)
-- config module.
local msg = self.cfg.msg[key]
if select(1, ...) then
return mw.message.newRawMessage(msg, ...):plain()
else
return msg
end
end
 
-------------------------------------------------------------------------------
-- Template class
-------------------------------------------------------------------------------
 
local Template = {}
 
Template.memoizedMethods = {
-- Names of methods to be memoized in each object. This table should only
-- hold methods with no parameters.
getFullPage = true,
getName = true,
makeHeader = true,
getOutput = true
}
 
function Template.new(invocationObj, options)
local obj = {}
 
-- Set input
for k, v in pairs(options or {}) do
if not Template[k] then
obj[k] = v
end
end
obj._invocation = invocationObj
 
-- Validate input
if not obj.template and not obj.title then
error('no template or title specified', 2)
end
 
-- Memoize expensive method calls
local memoFuncs = {}
return setmetatable(obj, {
__index = function (t, key)
if Template.memoizedMethods[key] then
local func = memoFuncs[key]
if not func then
local val = Template[key](t)
func = function () return val end
memoFuncs[key] = func
end
return func
else
return Template[key]
end
end
})
end
 
function Template:getFullPage()
if not self.template then
return self.title.prefixedText
elseif self.template:sub(1, 7) == '#invoke' then
return 'Module' .. self.template:sub(8):gsub('|.*', '')
else
local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
hasColon = hasColon > 0
local ns = strippedTemplate:match('^(.-):')
ns = ns and mw.site.namespaces[ns]
if ns then
return strippedTemplate
elseif hasColon then
return strippedTemplate -- Main namespace
else
return mw.site.namespaces[10].name .. ':' .. strippedTemplate
end
end
end
 
function Template:getName()
if self.template then
return self.template
else
return require('Module:Template invocation').name(self.title)
end
end
 
function Template:makeLink(display)
if display then
return string.format('[[:%s|%s]]', self:getFullPage(), display)
else
return string.format('[[:%s]]', self:getFullPage())
end
end
 
function Template:makeBraceLink(display)
display = display or self:getName()
local link = self:makeLink(display)
return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}')
end
 
function Template:makeHeader()
return self.heading or self:makeBraceLink()
end
 
function Template:getInvocation(format)
local invocation = self._invocation:getInvocation{
template = self:getName(),
requireMagicWord = self.requireMagicWord,
}
if format == 'code' then
invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>'
elseif format == 'kbd' then
invocation = '<kbd>' .. mw.text.nowiki(invocation) .. '</kbd>'
elseif format == 'plain' then
invocation = mw.text.nowiki(invocation)
else
-- Default is pre tags
invocation = mw.text.encode(invocation, '&')
invocation = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>'
invocation = mw.getCurrentFrame():preprocess(invocation)
end
return invocation
end
 
function Template:getOutput()
local protect = require('Module:Protect')
-- calling self._invocation:getOutput{...}
return protect(self._invocation.getOutput)(self._invocation, {
template = self:getName(),
requireMagicWord = self.requireMagicWord,
})
end
 
-------------------------------------------------------------------------------
-- TestCase class
-------------------------------------------------------------------------------
 
local TestCase = {}
TestCase.__index = TestCase
TestCase.message = message -- add the message method
 
TestCase.renderMethods = {
-- Keys in this table are values of the "format" option, values are the
-- method for rendering that format.
columns = 'renderColumns',
rows = 'renderRows',
tablerows = 'renderRows',
inline = 'renderInline',
cells = 'renderCells',
default = 'renderDefault'
}
 
function TestCase.new(invocationObj, options, cfg)
local obj = setmetatable({}, TestCase)
obj.cfg = cfg
 
-- Separate general options from template options. Template options are
-- numbered, whereas general options are not.
local generalOptions, templateOptions = {}, {}
for k, v in pairs(options) do
local prefix, num
if type(k) == 'string' then
prefix, num = k:match('^(.-)([1-9][0-9]*)$')
end
if prefix then
num = tonumber(num)
templateOptions[num] = templateOptions[num] or {}
templateOptions[num][prefix] = v
else
generalOptions[k] = v
end
end
 
-- Set general options
generalOptions.showcode = yesno(generalOptions.showcode)
generalOptions.showheader = yesno(generalOptions.showheader) ~= false
generalOptions.showcaption = yesno(generalOptions.showcaption) ~= false
generalOptions.collapsible = yesno(generalOptions.collapsible)
generalOptions.notcollapsed = yesno(generalOptions.notcollapsed)
generalOptions.wantdiff = yesno(generalOptions.wantdiff)  
obj.options = generalOptions
 
-- Preprocess template args
for num, t in pairs(templateOptions) do
if t.showtemplate ~= nil then
t.showtemplate = yesno(t.showtemplate)
end
end
 
-- Set up first two template options tables, so that if only the
-- "template3" is specified it isn't made the first template when the
-- the table options array is compressed.
templateOptions[1] = templateOptions[1] or {}
templateOptions[2] = templateOptions[2] or {}
 
-- Allow the "template" option to override the "template1" option for
-- backwards compatibility with [[Module:Testcase table]].
if generalOptions.template then
templateOptions[1].template = generalOptions.template
end
 
-- Add default template options
if templateOptions[1].template and not templateOptions[2].template then
templateOptions[2].template = templateOptions[1].template ..
'/' .. obj.cfg.sandboxSubpage
end
if not templateOptions[1].template then
templateOptions[1].title = mw.title.getCurrentTitle().basePageTitle
end
if not templateOptions[2].template then
templateOptions[2].title = templateOptions[1].title:subPageTitle(
obj.cfg.sandboxSubpage
)
end


== Sandbox2, Test case2, and wrapperConfig ==
-- Remove template options for any templates where the showtemplate
-- argument is false. This prevents any output for that template.
for num, t in pairs(templateOptions) do
if t.showtemplate == false then
templateOptions[num] = nil
end
end


I'd like the ability to use a second sandbox, say, 'sandbox2', to enable simultaneous testing of different sets of changes in parallel tracks for the same template. Having browsed [[Module:Template test case]], I believe a better method is available than the obvious brute force method of duplicating template FOO to FOO2, then FOO2/sandbox, and testcases to FOO2/testcases. Afaict the key seems to have something to do with {{var|wrapperConfig}} unless I'm mistaken, only I don't see how to use it, and the /doc doesn't have much to say about it.
-- Check for missing template names.
for num, t in pairs(templateOptions) do
if not t.template and not t.title then
error(obj:message(
'missing-template-option-error',
num, num
), 2)
end
end


This use case comes up occasionally in RL on complex templates that may need extended testing of some feature, while someone else desires to test another feature, or where different scenarios are being tried out. (For a RW case of the former type see [[Template talk:Find sources#Missing redirect detection bug]].) I get the feeling I can just create [[Template:Find sources/sandbox2]] with my changes, and then create a "wrapper config" somewhere, and set it as param2 to the invoke.
-- Compress templateOptions table so we can iterate over it with ipairs.
templateOptions = (function (t)
local nums = {}
for num in pairs(t) do
nums[#nums + 1] = num
end
table.sort(nums)
local ret = {}
for i, num in ipairs(nums) do
ret[i] = t[num]
end
return ret
end)(templateOptions)


For example, can I just duplicate [[Module:Template test case/config]] to Template:Find general sources/MySandbox2Config, edit it to set <code>sandboxSubpage = 'sandbox2',</code> and then in Template:Find general sources/sandbox2 invoke like this: {{tlc|#invoke:Find sources|Find general sources, MySandbox2Config}}? Or am I misconstruing how this is supposed to work, and I should go back to the brute force method? Or something else? Pinging {{ping|Mr. Stradivarius|Trialpears}}.  
-- Don't require the __TEMPLATENAME__ magic word for nowiki invocations if
-- there is only one template being output.
if #templateOptions <= 1 then
templateOptions[1].requireMagicWord = false
end


Either way, param2 of the module appears to be a config of some kind, so whether my assumptions above are right or wrong, it would be nice if someone could add a <code><nowiki>== Params ==</nowiki></code> section to [[Module:Template test case/doc]] to describe what this param is and how to use it. {{pme}} [[User:Mathglot|Mathglot]] ([[User talk:Mathglot|talk]]) 22:28, 25 December 2021 (UTC)
mw.logObject(templateOptions)


:{{reply|Mathglot}} You can use a second sandbox by setting the {{para|_template1}}, {{para|_template2}} and {{para|_template3}} arguments. For example, <code><nowiki>{{test case|_template1=Find sources|_template2=Find sources/sandbox|_template3=Find sources/sandbox2|Foo}}</nowiki></code> outputs the following:
-- Make the template objects
{{<nowiki>test case</nowiki>|_template1=Find sources|_template2=Find sources/sandbox|_template3=Find sources/sandbox2|Foo}}
obj.templates = {}
:The module is built so that the default value for _template1 is the base page of the current page, and the default value for _template2 is its sandbox subpage. In fact, if you are calling the module from [[Template:Find sources/testcases]], you could omit the {{para|_template1}} and {{para|_template2}} arguments, and just specify {{para|_template3|Find sources/sandbox2}} to get the same results. — '''''[[User:Mr. Stradivarius|<span style="color: #194D00; font-family: Palatino, Times, serif">Mr.&nbsp;Stradivarius</span>]]''''' <sup>[[User talk:Mr. Stradivarius|♪&nbsp;talk&nbsp;♪]]</sup> 01:17, 26 December 2021 (UTC)
for i, options in ipairs(templateOptions) do
::As for the config file, this is not intended to be specified by users. I believe I allowed the config to be passed to the main function so that it can be more easily tested, but this ability is not currently used by [[Module:Template test case/testcases]]. I probably only used it in the console during initial development. Also, the config cannot be passed from wikitext; it needs to be passed from another Lua module. From wikitext you can only influence the contents of the first parameter (the frame object) to the function called from #invoke; you can't influence the contents of subsequent parameters. — '''''[[User:Mr. Stradivarius|<span style="color: #194D00; font-family: Palatino, Times, serif">Mr.&nbsp;Stradivarius</span>]]''''' <sup>[[User talk:Mr. Stradivarius|♪&nbsp;talk&nbsp;♪]]</sup> 01:45, 26 December 2021 (UTC)
table.insert(obj.templates, Template.new(invocationObj, options))
::: Thanks, [[User:Mr. Stradivarius|Mr. Stradivarius]], this was very helpful. [[User:Mathglot|Mathglot]] ([[User talk:Mathglot|talk]]) 17:26, 26 December 2021 (UTC)
end


== format=columns + inline hybrid ==
-- Add tracking categories. At the moment we are only tracking templates
I've a bunch of test cases for a template that takes lots of complicated parameters, but whose output is a relatively short text. Think {{tl|cite book}} and you're in the ballpark. I want to see the code up top in a nowiki block like {{tlx|test case nowiki|format{{=}}columns}} or {{para|format|rows}} gives me, but I'd like to see the output arranged vertically like {{tlx|test case nowiki|format{{=}}inline}} gives me.
-- that use any "heading" parameters or an "output" parameter.
obj.categories = {}
for k, v in pairs(options) do
if type(k) == 'string' and k:find('heading') then
obj.categories['Test cases using heading parameters'] = true
elseif k == 'output' then
obj.categories['Test cases using output parameter'] = true
end
end
 
return obj
end


Something like:
function TestCase:getTemplateOutput(templateObj)
{{cot|Example of desired output}}
local output = templateObj:getOutput()
<code><pre><nowiki>{{example
if self.options.resetRefs then
|first-argument = has a value
mw.getCurrentFrame():extensionTag('references')
|second-argument = does too
end
|veni-vidi-vici = yes
return output
|lipsum = lorem
end
}}</nowiki></pre></code>
 
* ''Veni, vidi, vici''
function TestCase:templateOutputIsEqual()
* ''Lorem ipsum dolor sit amet''
-- Returns a boolean showing whether all of the template outputs are equal.
{{cob}}
-- The random parts of strip markers (see [[Help:Strip markers]]) are
-- removed before comparison. This means a strip marker can contain anything
-- and still be treated as equal, but it solves the problem of otherwise
-- identical wikitext not returning as exactly equal.
local function normaliseOutput(obj)
local out = obj:getOutput()
-- Remove the random parts from strip markers.
out = out:gsub('(\127[^\127]*UNIQ%-%-%l+%-)%x+(%-%-?QINU[^\127]*\127)', '%1%2')
return out
end
local firstOutput = normaliseOutput(self.templates[1])
for i = 2, #self.templates do
local output = normaliseOutput(self.templates[i])
if output ~= firstOutput then
return false
end
end
return true
end


Any takers? [[User:Mr. Stradivarius|Mr. Stradivarius]]? --[[User:Xover|Xover]] ([[User talk:Xover|talk]]) 08:45, 13 January 2023 (UTC)
function TestCase:makeCollapsible(s)
:{{tl|Testcase table}} with {{para|_format}}? – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 22:26, 14 January 2023
local title = self.options.title or self.templates[1]:makeHeader()
::Example:
if self.options.titlecode then
{{test case table|_showcode=yes|_format=vertical|_showheader=no|_collapsible=yes|_template1=code|1=foo}}
title = self.templates[1]:getInvocation('kbd')
::How's that? You can tweak the underscore parameters to your liking. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 23:34, 14 January 2023 (UTC)
end
:::Thanks. That's a nice option, but I kinda prefer the interface for {{tl|test case nowiki}} for this (it's clearer, and with less faffing and things that can go wrong when converting something found in the wild into a test case). --[[User:Xover|Xover]] ([[User talk:Xover|talk]]) 19:33, 15 January 2023 (UTC)
local isEqual = self:templateOutputIsEqual()
local root = mw.html.create('div')
root
:addClass('mw-collapsible')
:css('width', '100%')
:css('border', 'solid silver 1px')
:css('padding', '0.2em')
:css('clear', 'both')
:addClass(self.options.notcollapsed == false and 'mw-collapsed' or nil)
if self.options.wantdiff then
root
:tag('div')
:css('background-color', isEqual and 'yellow' or '#90a8ee')
:css('color', 'black')
:css('font-weight', 'bold')
:css('padding', '0.2em')
:wikitext(title)
:done()
else
if self.options.notcollapsed ~= true or false then
root
:addClass(isEqual and 'mw-collapsed' or nil)
end
root
:tag('div')
:css('background-color', isEqual and 'lightgreen' or 'yellow')
:css('color', 'black')
:css('font-weight', 'bold')
:css('padding', '0.2em')
:wikitext(title)
:done()
end
root
:tag('div')
:addClass('mw-collapsible-content')
:newline()
:wikitext(s)
:newline()
return tostring(root)
end


== Use of _before and _after ==
function TestCase:renderColumns()
local root = mw.html.create()
if self.options.showcode then
root
:wikitext(self.templates[1]:getInvocation())
:newline()
end


I'm trying to use the {{para|_before}} parameter, mostly to have something to hange templates that create superscript references, tags, and that sort of thing, so they don't appear to be isolated in space.  What I'd like to see in this example, is something like this:
local tableroot = root:tag('table')
: Some text to hang a tag on.{{Citation needed|reason=Some reason.|date=January 2023}}
Here's my test case, using {{para|_before}}:


<div style="border:2px solid PaleVioletRed; margin:0 0 0.5em 1.6em;padding:0.2em 1em;background-color:#D8FED8">
if self.options.showheader then
-- Caption
if self.options.showcaption then
tableroot
:addClass(self.options.class)
:cssText(self.options.style)
:tag('caption')
:wikitext(self.options.caption or self:message('columns-header'))
end


{{Test case|_before=Some text to hang a tag on.|_code=on|_template1=Citation needed|_template2=Citation needed/sandbox|reason=Some reason.|date=January 2023}}
-- Headers
</div>
local headerRow = tableroot:tag('tr')
if self.options.rowheader then
-- rowheader is correct here. We need to add another th cell if
-- rowheader is set further down, even if heading0 is missing.
headerRow:tag('th'):wikitext(self.options.heading0)
end
local width
if #self.templates > 0 then
width = tostring(math.floor(100 / #self.templates)) .. '%'
else
width = '100%'
end
for i, obj in ipairs(self.templates) do
headerRow
:tag('th')
:css('width', width)
:wikitext(obj:makeHeader())
end
end


Am I doing something wrong, here? Why isn't it echoing the "before" text? [[User:Mathglot|Mathglot]] ([[User talk:Mathglot|talk]]) 04:29, 26 January 2023 (UTC)
-- Row header
:{{ping|Mathglot}} You aren't doing anything wrong. It seems the _before (and _after) option wasn't implemented for all render (_format) methods (There's 5, which are columns, rows, inline, cells, and default - only columns and cells considered _before, and your example renders in default since nothing was specified). I've implemented the code into the other render formats, so it should start displaying now. [[User:Aidan9382|Aidan9382]] <sub>([[User talk:Aidan9382|talk]])</sub> 07:10, 26 January 2023 (UTC)
local dataRow = tableroot:tag('tr'):css('vertical-align', 'top')
:: [[User:Aidan9382|Aidan9382]] And, indeed it is! I noticed it working on one of my testcase pages, before I even saw my notification alert icon change color. Any faster, and I'd have to give you the faster-than-light barnstar {{wink}}; thanks so much! (For the curious: the example above used to show nothing but tags, and no text; but since Aidan's fix, is now working properly.) [[User:Mathglot|Mathglot]] ([[User talk:Mathglot|talk]]) 07:21, 26 January 2023 (UTC)
if self.options.rowheader then
dataRow:tag('th')
:attr('scope', 'row')
:wikitext(self.options.rowheader)
end
-- Template output
for i, obj in ipairs(self.templates) do
if self.options.output == 'nowiki+' then
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
:wikitext('<pre style="white-space: pre-wrap;">')
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
:wikitext('</pre>')
elseif self.options.output == 'nowiki' then
dataRow:tag('td')
:newline()
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
else
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
end
end
return tostring(root)
end


== Template use when there's no sandbox ==
function TestCase:renderRows()
local root = mw.html.create()
if self.options.showcode then
root
:wikitext(self.templates[1]:getInvocation())
:newline()
end


@[[User:Frietjes|Frietjes]], re [[Special:Diff/1155772824|this edit]], should we make some change to {{t|Test case}} to better handle instances when it's used when there's no sandbox? <span style="color:#AAA"><small>&#123;{u&#124;</small><span style="border-radius:9em;padding:0 5px;background:#088">[[User:Sdkb|<span style="color:#FFF">'''Sdkb'''</span>]]</span><small>}&#125;</small></span> <sup>[[User talk:Sdkb|'''talk''']]</sup> 19:42, 19 May 2023 (UTC)
local tableroot = root:tag('table')
: {{u|Sdkb}}, maybe, or you could just create a sandbox version. [[User:Frietjes|Frietjes]] ([[User talk:Frietjes|talk]]) 19:43, 19 May 2023 (UTC)
tableroot
::True. In that case, I was just looking to see whether the template could handle a particular use case, not to make changes to it, so all I needed was the testcases page. <span style="color:#AAA"><small>&#123;{u&#124;</small><span style="border-radius:9em;padding:0 5px;background:#088">[[User:Sdkb|<span style="color:#FFF">'''Sdkb'''</span>]]</span><small>}&#125;</small></span> <sup>[[User talk:Sdkb|'''talk''']]</sup> 19:46, 19 May 2023 (UTC)
:addClass(self.options.class)
:::I always just click the "mirror" link in the template doc when the sandbox does not exist. It takes two clicks total to create a working sandbox. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 15:14, 21 May 2023 (UTC)
:cssText(self.options.style)


== Using syntaxhighlight when _showcode is used ==
if self.options.caption then
tableroot
:tag('caption')
:wikitext(self.options.caption)
end


I've changed the {{tag|code}} tags to {{tag|syntaxhighlight}} tags when {{para|_showcode}} is used. [[Module:Template test case/sandbox|Sandbox]] and [https://en.wikipedia.org/w/index.php?title=Special%3AComparePages&page1=Module%3ATemplate+test+case&page2=Module%3ATemplate+test+case%2Fsandbox Diff]. [[User:Gonnym|Gonnym]] ([[User talk:Gonnym|talk]]) 18:04, 23 May 2023 (UTC)
for _, obj in ipairs(self.templates) do
local dataRow = tableroot:tag('tr')
-- Header
if self.options.showheader then
if self.options.format == 'tablerows' then
dataRow:tag('th')
:attr('scope', 'row')
:css('vertical-align', 'top')
:css('text-align', 'left')
:wikitext(obj:makeHeader())
dataRow:tag('td')
:css('vertical-align', 'top')
:css('padding', '0 1em')
:wikitext('→')
else
dataRow:tag('td')
:css('text-align', 'center')
:css('font-weight', 'bold')
:wikitext(obj:makeHeader())
dataRow = tableroot:tag('tr')
end
end
-- Template output
if self.options.output == 'nowiki+' then
dataRow:tag('td')
:newline()
                :wikitext(self.options.before)
                :wikitext(self:getTemplateOutput(obj))
                :wikitext(self.options.after)
                :wikitext('<pre style="white-space: pre-wrap;">')
                :wikitext(mw.text.nowiki(self.options.before or ""))
                :wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
                :wikitext(mw.text.nowiki(self.options.after or ""))
                :wikitext('</pre>')
elseif self.options.output == 'nowiki' then
dataRow:tag('td')
:newline()
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
else
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
end
end


:Hmm. Would it make sense to have that be opt-in, at least initially, by adding a new {{para|format|syntaxhighlight}} (and/or {{para|format|syntax}} maybe) instead? Are there any weird border cases where {{tag|code|content={{tag|nowiki}}}} works, but {{tag|syntaxhighlight|attribs=lang{{=}}"mediawiki" inline}} might break? [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 04:00, 15 July 2023 (UTC)
return tostring(root)
end


== Inconsistency between Test case nowiki and others ==
function TestCase:renderInline()
local arrow = mw.language.getContentLanguage():getArrow('forwards')
local ret = {}
for i, obj in ipairs(self.templates) do
local line = {}
line[#line + 1] = self.options.prefix or '* '
if self.options.showcode then
line[#line + 1] = obj:getInvocation('code')
line[#line + 1] = ' '
line[#line + 1] = arrow
line[#line + 1] = ' '
end
if self.options.output == 'nowiki+' then
line[#line + 1] = self.options.before or ""
line[#line + 1] = self:getTemplateOutput(obj)
line[#line + 1] = self.options.after or ""
line[#line + 1] = '<pre style="white-space: pre-wrap;">'
line[#line + 1] = mw.text.nowiki(self.options.before or "")
line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
line[#line + 1] = mw.text.nowiki(self.options.after or "")
line[#line + 1] = '</pre>'
elseif self.options.output == 'nowiki' then
line[#line + 1] = mw.text.nowiki(self.options.before or "")
line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
line[#line + 1] = mw.text.nowiki(self.options.after or "")
else
line[#line + 1] = self.options.before or ""
line[#line + 1] = self:getTemplateOutput(obj)
line[#line + 1] = self.options.after or ""
end
ret[#ret + 1] = table.concat(line)
end
if self.options.addline then
local line = {}
line[#line + 1] = self.options.prefix or '* '
line[#line + 1] = self.options.addline
ret[#ret + 1] = table.concat(line)
end
return table.concat(ret, '\n')
end


So, in most of the test case templates, you pass arguments to the test-case code preceded by underscores, and actual arguments to the template being tested are passed as usual, e.g.:
function TestCase:renderCells()
<syntaxhighlight lang="mediawiki">
local root = mw.html.create()
{{Test case|_collapsible=yes|_showcode=yes|_title=Some test case
local dataRow = root:tag('tr')
|_template1=Code
dataRow
|1=wikitable
:css('vertical-align', 'top')
}}
:addClass(self.options.class)
</syntaxhighlight>
:cssText(self.options.style)


OK, great. But then here comes {{tlx|Test case nowiki}}, where the underscore arguments don't work, and you have to use non-underscored ones:
-- Row header
if self.options.rowheader then
dataRow:tag('th')
:attr('scope', 'row')
:newline()
:wikitext(self.options.rowheader or self:message('row-header'))
end
-- Caption
if self.options.showcaption then
dataRow:tag('th')
:attr('scope', 'row')
:newline()
:wikitext(self.options.caption or self:message('columns-header'))
end


===BAD===
-- Show code
<syntaxhighlight lang="mediawiki">
if self.options.showcode then
{{Test case nowiki|_collapsible=yes|_showcode=yes|_title=Some test case
dataRow:tag('td')
|_template1=Code
:newline()
|<nowiki>{{__TEMPLATENAME__
:wikitext(self:getInvocation('code'))
|1=wikitable}}</nowiki>
end
}}
</syntaxhighlight>


===Works===
-- Template output
<syntaxhighlight lang="mediawiki">
for i, obj in ipairs(self.templates) do
{{Test case nowiki|collapsible=yes|showcode=yes|title=Some test case
if self.options.output == 'nowiki+' then
|template1=Code
dataRow:tag('td')
|<nowiki>{{__TEMPLATENAME__
:newline()
|1=wikitable}}</nowiki>
:wikitext(self.options.before)
}}
:wikitext(self:getTemplateOutput(obj))
</syntaxhighlight>
:wikitext(self.options.after)
:wikitext('<pre style="white-space: pre-wrap;">')
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
:wikitext('</pre>')
elseif self.options.output == 'nowiki' then
dataRow:tag('td')
:newline()
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
else
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
end
end


My question is, '''''why'''''??? If these templates are a "family", and so often used together, doesn't it make sense for {{tlx|Test case nowiki}} to take the same set of arguments that {{tlx|Test case}} and all the others take? Even if it's not strictly ''necessary'' for them to be preceded by underscores? It would make converting test cases between the non-nowiki and nowiki versions a lot quicker and more convenient. Am I really the only person who finds themselves doing that pretty frequently? [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 06:33, 15 July 2023 (UTC)
return tostring(root)
end


:The technical reason for why comes down to [[Module:Template test case#L-770|this bit of code]], in which the nowiki wrapper has its arguments done differently. The reason I think this was done in practice is because the only reason the underscores are used in the {{tl|Test case}} version is because you have to provide the real arguments, so prefixing the options with _ prevents conflicts. Since the nowiki version just takes the parameter 1, it had no need for the underscores. [[User:Aidan9382|Aidan9382]] <sub>([[User talk:Aidan9382|talk]])</sub> 06:57, 15 July 2023 (UTC)
function TestCase:renderDefault()
::<nowiki>*nod*</nowiki> I totally understand why the _-prefixed arguments are needed for the other templates, and why {{tlx|Test case nowiki}} can do without them. But in the interests of... I don't know, consistency, harmony, whatever, it seems like <code>bridge.nowiki</code> could include this logic:<syntaxhighlight lang="lua">
local ret = {}
local options = {}
if self.options.showcode then
for k, v in pairs(args) do
ret[#ret + 1] = self.templates[1]:getInvocation()
local underscoreOptionKey = type(k) == 'string' and k:match('^_(.*)$')
end
if underscoreOptionKey then
for i, obj in ipairs(self.templates) do
options[underscoreOptionKey] = v
ret[#ret + 1] = '<div style="clear: both;"></div>'
if self.options.showheader then
ret[#ret + 1] = obj:makeHeader()
end
if self.options.output == 'nowiki+' then
ret[#ret + 1] = (self.options.before or "") ..
self:getTemplateOutput(obj) ..
(self.options.after or "") ..
'<pre style="white-space: pre-wrap;">' ..
mw.text.nowiki(self.options.before or "") ..
mw.text.nowiki(self:getTemplateOutput(obj)) ..
mw.text.nowiki(self.options.after or "") .. '</pre>'
elseif self.options.output == 'nowiki' then
ret[#ret + 1] = mw.text.nowiki(self.options.before or "") ..
mw.text.nowiki(self:getTemplateOutput(obj)) ..
mw.text.nowiki(self.options.after or "")
else
else
options[k] = v
ret[#ret + 1] = (self.options.before or "") ..
self:getTemplateOutput(obj) ..
(self.options.after or "")
end
end
end
end
local code = options.code or options[1]
return table.concat(ret, '\n\n')
local invocationObj = NowikiInvocation.new(code, cfg)
end
options.code = nil
 
options[1] = nil
function TestCase:__tostring()
-- Assume we want to see the code as we already passed it in.
local format = self.options.format
options.showcode = options.showcode or true
local method = format and TestCase.renderMethods[format] or 'renderDefault'
local testCaseObj = TestCase.new(invocationObj, options, cfg)</syntaxhighlight>
local ret = self[method](self)
::...So that e.g. {{para|_collapsible}} and {{para|collapsible}} are equivalent, meaning the user has the option to pass the exact same arguments as all the other templates in the group. [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 18:44, 15 July 2023 (UTC)
if self.options.collapsible then
:::Thats fair, I've gone ahead and implemented that [[Special:Diff/1165527145|here]]. Either option should work now. [[User:Aidan9382|Aidan9382]] <sub>([[User talk:Aidan9382|talk]])</sub> 19:04, 15 July 2023 (UTC)
ret = self:makeCollapsible(ret)
::::This change appears to have caused, or may have caused, an error in some test cases. The one that came to my attention is {{tl|Circular reporting/testcases}}, where some "span title" code is being exposed. My wild guess is that a rendered equals sign in _title may be causing the problem. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 21:15, 16 July 2023 (UTC)
end
:::::This change right here shouldn't of (and hasn't) caused the issue, since this only effects how args are processed in {{tl|Test case nowiki}}, and the normal {{tl|Test case}}'s behaviour is unchanged. I suspect that specific test case has been broken for a while. I've fixed it [[Special:Diff/1136399202/1165754453|here]] and [[Special:Diff/1165754514|here]], and it should now work fine. [[User:Aidan9382|Aidan9382]] <sub>([[User talk:Aidan9382|talk]])</sub> 06:04, 17 July 2023 (UTC)
for cat in pairs(self.categories) do
::::::Strange. I wonder why that page popped into the error report right after the change, and why this Module shows in [https://en.wikipedia.org/wiki/Special:RecentChangesLinked?hidebots=1&hidecategorization=1&hideWikibase=1&target=Template%3ACircular_reporting%2Ftestcases&limit=1000&days=30&enhanced=1&urlversion=2 Related changes] for the testcases page. It is also odd that this module appears in "Pages transcluded onto the current version of this page" when you edit the testcases page. Maybe it's a coincidence. P.S. I have modified both of the changes, since one of them caused Linter errors in other places. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 12:52, 17 July 2023 (UTC)
ret = ret .. string.format('[[Category:%s]]', cat)
:::::::{{tq|It is also odd that this module appears in "Pages transcluded onto the current version of this page" when you edit the testcases page}} - that part makes sense, since {{tl|Test case}} uses this module, as does {{tl|Test case nowiki}} and a couple of the other related templates. As for why it took editing this for it to appear in an error report, even I'm not sure on that one. It definitely didn't change the behaviour, so maybe this change just caused it to purge and appear in the report. Could the report have had a recent change on what it picks up? [[User:Aidan9382|Aidan9382]] <sub>([[User talk:Aidan9382|talk]])</sub> 13:00, 17 July 2023 (UTC)
end
::::Many, many thanks, {{u|Aidan9382}}, for putting this in place. [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 14:52, 20 July 2023 (UTC)
return ret
end
 
-------------------------------------------------------------------------------
-- Nowiki invocation class
-------------------------------------------------------------------------------
 
local NowikiInvocation = {}
NowikiInvocation.__index = NowikiInvocation
NowikiInvocation.message = message -- Add the message method


== Extended-confirmed-protected edit request on 17 October 2023 ==
function NowikiInvocation.new(invocation, cfg)
local obj = setmetatable({}, NowikiInvocation)
obj.cfg = cfg
invocation = mw.text.unstrip(invocation)
-- Decode HTML entities for <, >, and ". This means that HTML entities in
-- the original code must be escaped as e.g. &amp;lt;, which is unfortunate,
-- but it is the best we can do as the distinction between <, >, " and &lt;,
-- &gt;, &quot; is lost during the original nowiki operation.
invocation = invocation:gsub('&lt;', '<')
invocation = invocation:gsub('&gt;', '>')
invocation = invocation:gsub('&quot;', '"')
obj.invocation = invocation
return obj
end


{{edit extended-protected|Module:Template test case/config|answered=yes}}
function NowikiInvocation:getInvocation(options)
Please, remove the '''n''' of the word '''templaten''' in the line number 29. [[User:Gkiyoshinishimoto|Nishimoto, Gilberto Kiyoshi]] ([[User talk:Gkiyoshinishimoto|talk]]) 16:38, 17 October 2023 (UTC)
local template = options.template:gsub('%%', '%%%%') -- Escape "%" with "%%"
:{{done}} [[User:Aidan9382|Aidan9382]] <sub>([[User talk:Aidan9382|talk]])</sub> 16:42, 17 October 2023 (UTC)
local invocation, count = self.invocation:gsub(
self.cfg.templateNameMagicWordPattern,
template
)
if options.requireMagicWord ~= false and count < 1 then
error(self:message(
'nowiki-magic-word-error',
self.cfg.templateNameMagicWord
))
end
return invocation
end


== Trim input's newlines? ==
function NowikiInvocation:getOutput(options)
local invocation = self:getInvocation(options)
return mw.getCurrentFrame():preprocess(invocation)
end


Many people use {{tl|test case nowiki}} as follows for aesthetics:
-------------------------------------------------------------------------------
<syntaxhighlight lang="wikitext">
-- Table invocation class
{{test case nowiki|<nowiki>
-------------------------------------------------------------------------------
{{wrapper template top}}
{{__TEMPLATENAME}}
{{wrapper template bottom}}
</nowiki>}}
</syntaxhighlight>
This causes an extra newline to be inserted before and after the template's part inside the test case frame. It would be wonderful if this module would trim the input's newlines. [[User:Aaron Liu|<span style="color:#0645ad">Aaron Liu</span>]] ([[User talk:Aaron Liu#top|talk]]) 15:52, 8 August 2024 (UTC)
: Do you mean to trim newlines in a way that would make <syntaxhighlight lang="wikitext">
{{test case nowiki|<nowiki>
{{wrapper template top}}
{{__TEMPLATENAME}}
{{wrapper template bottom}}
</nowiki>}}
</syntaxhighlight>
:equivalent to
: <syntaxhighlight lang="wikitext">
{{test case nowiki|<nowiki>{{wrapper template top}}
{{__TEMPLATENAME}}
{{wrapper template bottom}}</nowiki>}}
</syntaxhighlight>
:? That is, trim newlines which are right next to the outer pair of {{tag|nowiki}} tags. —⁠[[User:Andrybak|andrybak]] ([[User talk:Andrybak|talk]]) 22:47, 8 August 2024 (UTC)
::Yes, thanks. [[User:Aaron Liu|<span style="color:#0645ad">Aaron Liu</span>]] ([[User talk:Aaron Liu#top|talk]]) 23:28, 8 August 2024 (UTC)


== Having multiple __TEMPLATENAME__s ==
local TableInvocation = {}
TableInvocation.__index = TableInvocation
TableInvocation.message = message -- Add the message method


I wanted to gauge interest in possibly extending the Module code to (optionally) support testing nowiki'd code that contains ''multiple'' template name substitutions. The primary use case would be testing templates that are only useful in concert with others, like (for example) {{tlx|archive top}}.
function TableInvocation.new(invokeArgs, nowikiCode, cfg)
local obj = setmetatable({}, TableInvocation)
obj.cfg = cfg
obj.invokeArgs = invokeArgs
obj.code = nowikiCode
return obj
end


It's certainly possible to test that template in isolation, using a {{tlx|Test case nowiki}} transclusion at [[Template:Archive top/testcases]] like:
function TableInvocation:getInvocation(options)
if self.code then
local nowikiObj = NowikiInvocation.new(self.code, self.cfg)
return nowikiObj:getInvocation(options)
else
return require('Module:Template invocation').invocation(
options.template,
self.invokeArgs
)
end
end


<syntaxhighlight lang="mediawiki">
function TableInvocation:getOutput(options)
{{Test case nowiki|<nowiki>
if (options.template:sub(1, 7) == '#invoke') then
{{__TEMPLATENAME__}}
local moduleCall = mw.text.split(options.template, '|', true)
{{Lorem ipsum}}
local args = mw.clone(self.invokeArgs)
{{archive bottom}}
table.insert(args, 1, moduleCall[2])
</nowiki>}}
return mw.getCurrentFrame():callParserFunction(moduleCall[1], args)
</syntaxhighlight>
end
return mw.getCurrentFrame():expandTemplate{
title = options.template,
args = self.invokeArgs
}
end


...That'll get you a perfectly good test of {{tlx|archive top}} vs. {{tlx|archive top/sandbox}}.
-------------------------------------------------------------------------------
-- Bridge functions
--
-- These functions translate template arguments into forms that can be accepted
-- by the different classes, and return the results.
-------------------------------------------------------------------------------


But it might be useful, at times, to pair the tested {{tlx|archive top}} with a '''corresponding''' {{tlx|archive bottom}}, for when changes being made in tandem are being tested.
local bridge = {}


To meet those needs, the Module might need to support something like the following:
function bridge.table(args, cfg)
cfg = cfg or mw.loadData(DATA_MODULE)


<syntaxhighlight lang="mediawiki">
local options, invokeArgs = {}, {}
{{Test case nowiki|template1name1=archive top|template1name2=archive bottom|<nowiki>
for k, v in pairs(args) do
{{__TEMPLATENAME1__}}
local optionKey = type(k) == 'string' and k:match('^_(.*)$')
{{Lorem ipsum}}
if optionKey then
{{__TEMPLATENAME2__}}
if type(v) == 'string' then
</nowiki>}}
v = v:match('^%s*(.-)%s*$') -- trim whitespace
</syntaxhighlight>
end
if v ~= '' then
options[optionKey] = v
end
else
invokeArgs[k] = v
end
end


This would, my thinking goes, permit testing the '''''combination''''' of {{tlx|archive top}} and {{tlx|archive bottom}} against the '''''combination''''' of {{tlx|archive top/sandbox}} and {{tlx|archive bottom/sandbox}} (unless different templates were specified for {{para|template2name1}} and {{para|template2name2}}).
-- Allow passing a nowiki invocation as an option. While this means users
-- have to pass in the code twice, whitespace is preserved and &lt; etc.
-- will work as intended.
local nowikiCode = options.code
options.code = nil


As with the citation templates' handling of {{para|author}}, {{para|first}}, {{para|last}}, etc.:
local invocationObj = TableInvocation.new(invokeArgs, nowikiCode, cfg)
local testCaseObj = TestCase.new(invocationObj, options, cfg)
return tostring(testCaseObj)
end


# {{para|template1}} would effectively become an alias for {{para|template1name1}} (with the same default of <syntaxhighlight lang="mediawiki" inline>{{#titleparts: {{PAGENAME}}| -1}}</syntaxhighlight>)
function bridge.nowiki(args, cfg)
# Same for {{para|template2}} and {{para|template2name1}} (again having the same default of <syntaxhighlight lang="mediawiki" inline>{{{template1name1}}}/sandbox</syntaxhighlight>)
cfg = cfg or mw.loadData(DATA_MODULE)
# {{para|template3}} == {{para|template3name1}}, etc.
# <code>__TEMPLATENAME__</code> would be interchangeable with <code>__TEMPLATENAME1__</code>.
-- Convert args beginning with _ for consistency with the normal bridge
local newArgs = {}
for k, v in pairs(args) do
local normalName = type(k) == "string" and string.match(k, "^_(.*)$")
if normalName then
newArgs[normalName] = v
else
newArgs[k] = v
end
end


Using only {{para|template1name1}}, {{para|template2name1}}, and <code>__TEMPLATENAME1__</code> would therefore be equivalent to the current syntax, as would using ''only'' <code>__TEMPLATENAME1__</code> and leaving the template-selection parameters at their default values.
local code = newArgs.code or newArgs[1]
local invocationObj = NowikiInvocation.new(code, cfg)
newArgs.code = nil
newArgs[1] = nil
-- Assume we want to see the code as we already passed it in.
newArgs.showcode = newArgs.showcode or true
local testCaseObj = TestCase.new(invocationObj, newArgs, cfg)
return tostring(testCaseObj)
end


...Potentially useful? Some flaw I've overlooked in the proposed functionality? Or just not worth the effort, regardless whether or not it would work? [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 18:21, 29 August 2024 (UTC)
-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------


:I've since decided that the parameter names {{para|template1name1}} and {{para|template1name2}} feel awkward and confusing. They '''should''' be {{para|template1name1}} and {{para|template2name1}}, if anything, but that won't work because {{para|template1}} and {{para|template2}} are existing parameters that have different, incompatible definitions in the code.
local p = {}
:Nevertheless, a call using e.g. {{para|template1name1|archive top}}{{para|template1name2|archive bottom}} just feels confusing. (They're '''not''' different ''names'' for the first template, they're ''different '''templates''''' to be used in the first rendering of the test case.)
:So to avoid that confusion, I think it would be better to make {{para|templatename1}} the new equivalent for {{para|template1}}, instead. The second template to use in rendering the test case would be just {{para|templatename2}}. {{para|template2name1}} would still be equivalent to {{para|template2}}, and {{para|template2name2}} would still be the corresponding equivalent to {{para|templatename2}} for the second rendering of the case. (Though often they'd not be specified, and default to the {{code|/sandbox}} versions of the templates specified for {{para|templatename1}} .. {{para|templatename<var>N</var>}}. The notion of a "{{param|template1}}" parameter would be a deprecated compatibility labeling — the "proper"/"preferred" identifiers would be {{param|templatename1}} ... {{param|templatename<var>N</var>}}. Which, handily, would also exactly correspond to the placeholder strings {{code|__TEMPLATENAME1__}} ... <code>__TEMPLATENAME<var>N</var>__</code> used to insert them into the nowiki'd code.
:The other option would be {{para|case1template1}} ... {{para|case1template<var>N</var>}} for the first rendering, then {{para|case2template1}} ... {{para|case2template<var>N</var>}} for the normally-defaulted-to-<code>/sandbox</code> versions, and so on... But that's a much bigger and more disruptive change that doesn't feel worth the upheaval. (It also changes the meaning of "template1" vs. "template2", compared to their current definition. So, still potentially confusing.) [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 03:47, 7 October 2024 (UTC)


== Why visual matches instead of string matches? ==
function p.main(frame, cfg)
cfg = cfg or mw.loadData(DATA_MODULE)


Sorry for what must be a silly question, but why does this tool require visual comparison rather than doing a match and emitting '''TEST FAIL!''' [[User:Johnjbarton|Johnjbarton]] ([[User talk:Johnjbarton|talk]]) 22:19, 6 October 2024 (UTC)
-- Load the wrapper config, if any.
local wrapperConfig
if frame.getParent then
local title = frame:getParent():getTitle()
local template = title:gsub(cfg.sandboxSubpagePattern, '')
wrapperConfig = cfg.wrappers[template]
end


:Not sure I follow — a string comparison of the output is precisely what the module's <syntaxhighlight lang="lua" inline>TestCase:templateOutputIsEqual()</syntaxhighlight> function does; the {{tlx|Collapsible test case}} template — or any of the templates, with {{para|_collapsible|yes}} set — will even automatically collapse any testcase where the results are the same. (As well as coloring the top bar of the collapsible box green (same output) vs. yellow (differences detected).)
-- Work out the function we will call, use it to generate the config for
:However, it's often the case that differences between the main version of a template and its sandbox version are '''intentional'''. (They'll be present whenever development is being done on the sandbox version of the code, for example.) So, characterizing differences as "TEST FAIL!" isn't strictly accurate, as having the same output isn't "success" and having ''different'' output isn't "failure". The point, when there are differences, is to demonstrate/examine how/whether the sandbox version has '''improved''' the output, vs. the live code. (If so, then a request can be made to transfer the sandbox code to the live template. At which point, the differences go away.) [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 23:38, 6 October 2024 (UTC)
-- Module:Arguments, and use Module:Arguments to find the arguments passed
:(Template test cases are therefore not like traditional programming [[unit test]]s, where the result of some code is compared to a known expected value, and any deviation represents a failure to perform as expected. Template test cases compare and contrast two versions of the same template, and any differences in output are ''interesting'' as a means of evaluating the code changes that created those differences.) [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 23:51, 6 October 2024 (UTC)
-- by the user.
::Thanks! Sorry, I only read up to the Collapsible test cases and stopped, because I was far from wanting to collapse test cases. I did not see that how fails are alerted. [[User:Johnjbarton|Johnjbarton]] ([[User talk:Johnjbarton|talk]]) 00:01, 7 October 2024 (UTC)
local func = wrapperConfig and wrapperConfig.func or 'table'
local userArgs = require('Module:Arguments').getArgs(frame, {
parentOnly = wrapperConfig,
frameOnly = not wrapperConfig,
trim = func ~= 'table',
removeBlanks = func ~= 'table'
})


== Empty testcases ==
-- Get default args and build the args table. User-specified args overwrite
-- default args.
local defaultArgs = wrapperConfig and wrapperConfig.args or {}
local args = {}
for k, v in pairs(defaultArgs) do
args[k] = v
end
for k, v in pairs(userArgs) do
args[k] = v
end


Maybe could someone give a hint why when using this template on local wiki it is not showing comparison tables as in enwiki? I have migrated same modules and code, but it is not showing anything, just some text: [[:lt:Template:Infolentelė/testcases]] [[User:Zygimantus|Zygimantus]] ([[User talk:Zygimantus|talk]]) 21:02, 13 October 2024 (UTC)
return bridge[func](args, cfg)
:Click on the triangle next to "Straipsnyje naudojami šablonai:" to see the templates that are being requested. You may need to create one or more of the red templates in order to make that page work. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 00:36, 15 October 2024 (UTC)
end
::Yes, I thought about that, I have another page: [[:lt:Šablonas:Sąrašas be punktų/testcases]] for that, this one is minimal and does not have redlinks, still no comparison is visible, maybe some kind of unknown gadget or plugin is used? [[User:Zygimantus|Zygimantus]] ([[User talk:Zygimantus|talk]]) 11:00, 15 October 2024 (UTC)
:::I am stuck then. Maybe something in one of the modules or templates is dependent on an English-language name of a page or namespace, but that is a guess. – [[User:Jonesey95|Jonesey95]] ([[User talk:Jonesey95|talk]]) 12:52, 17 October 2024 (UTC)
::::Will try something, it is a pity that no error message or something is visible, maybe then it is related to CSS styles... [[User:Zygimantus|Zygimantus]] ([[User talk:Zygimantus|talk]]) 21:15, 18 October 2024 (UTC)
:::::@[[User:Zygimantus|Zygimantus]] For starters I think you need to configure https://lt.wikipedia.org/wiki/Module:Template_test_case/config for the wiki — in particular, the <code>wrappers</code> table, which the module uses to map names of templates to the functions and arguments they should use.
:::::If you notice, all of the templates related to [[Module:Template test case]] all contain the same code:
:::::<syntaxhighlight lang="mediawiki">{{#invoke:Template test case|main}}</syntaxhighlight>
:::::That's because, when the module sees it's being invoked from e.g. [[Template:Testcase table]], the <code>wrappers</code> table tells it to automatically add <code>_format = 'columns'</code> to the arguments. But on your wiki the module will never be invoked from [[Template:Testcase table]], it'll be invoked from [[lt:Šablonas:Testcase table|Šablonas:Testcase table]], and there's no wrapper mapping for that <s>template</s> page name. That's, at the very least, why your test cases are being formatted as rows instead of columns.
:::::It may not be the ''whole'' issue, but it's definitely '''an''' issue.
:::::You're also missing [[Module:Suppress categories]], which the your [[lt:Šablonas:Suppress categories|Šablonas:Suppress categories]] depends on. (Discovered by editing the testcases page, then selecting "Templates used" in the hamburger menu and looking for redlinks in the resulting list — a handy way to root out broken dependencies. There are a few others.) [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 06:09, 25 October 2024 (UTC)
::::::{{Reply|FeRDNYC}} thanks. I had to translate „Template“ text in that config page. Also I wasn't aware that on local wikis these pages are not the same: [[:lt:Šablonas:Testcase table]] and [[:lt:Template:Testcase table]]. For example, if you click on those links, they will direct to same page, but on Module page it works not like that I suppose. [[User:Zygimantus|Zygimantus]] ([[User talk:Zygimantus|talk]]) 13:06, 25 October 2024 (UTC)
:::::::@[[User:Zygimantus|Zygimantus]] Yeah, the problem is that [[lt:Template:Testcase table]] acts as an '''alias''' for the canonical name — which works fine, going ''in'', because you end up at the same place. But it doesn't work in reverse, so when the module asks "What template am I being called from?", the answer will never be "Template:Testcase table"; that's not what the page is called. [[User:FeRDNYC|FeRDNYC]] ([[User talk:FeRDNYC|talk]]) 16:37, 25 October 2024 (UTC)


== Option to invoke a module ==
function p._exportClasses() -- For testing
return {
Template = Template,
TestCase = TestCase,
NowikiInvocation = NowikiInvocation,
TableInvocation = TableInvocation
}
end


It would be helpful if this module could be test module output. For example instead of {{para|_template=xyz}} we had something like {{para|_module|xyz}} and {{para|_function|def}} which would pass the arguments to <code><nowiki>{{#invoke:xyz|def|...</nowiki></code> and <code><nowiki>{{#invoke:xyz/sandbox|def|...</nowiki></code> and compare the results &mdash;&nbsp;Martin <small>([[User:MSGJ|MSGJ]]&nbsp;·&nbsp;[[User talk:MSGJ|talk]])</small> 21:54, 23 January 2025 (UTC)
return p

Revision as of 06:15, 3 February 2025

Lua error in package.lua at line 80: module 'Module:Yesno' not found. This module provides a framework for making templates which produce a template test case. While test cases can be made manually, using Lua-based templates such as the ones provided by this module has the advantage that the template arguments only need to be input once, thus reducing the effort involved in making test cases and reducing the possibility of errors in the input.

Usage

This module should not usually be called directly. Instead, you should use one of the following templates:

Parameter-based templates:

The only difference between these templates is their default arguments. For example, it is possible to display test cases side by side in Template:Testcase rows by specifying |_format=columns

Nowiki-based templates:

It is also possible to use a format of {{#invoke:template test case|main|parameters}}. This uses the same defaults as Template:Test case; please see that page for documentation of the parameters.

There is no direct interface to this module for other Lua modules. Lua modules should generally use Lua-based test case modules such as Module:UnitTests or Module:ScribuntoUnit. If it is really necessary to use this module, you can use frame:expandTemplate with one of the templates listed above.

Configuration

This module has a configuration module at Module:Template test case/config. You can edit it to add new wrapper templates, or to change the messages that the module outputs.

Tracking categories


--[[
   A module for generating test case templates.

   This module incorporates code from the English Wikipedia's "Testcase table"
   module,[1] written by Frietjes [2] with contributions by Mr. Stradivarius [3]
   and Jackmcbarn,[4] and the English Wikipedia's "Testcase rows" module,[5]
   written by Mr. Stradivarius.

   The "Testcase table" and "Testcase rows" modules are released under the
   CC BY-SA 3.0 License [6] and the GFDL.[7]

   License: CC BY-SA 3.0 and the GFDL
   Author: Mr. Stradivarius

   [1] https://en.wikipedia.org/wiki/Module:Testcase_table
   [2] https://en.wikipedia.org/wiki/User:Frietjes
   [3] https://en.wikipedia.org/wiki/User:Mr._Stradivarius
   [4] https://en.wikipedia.org/wiki/User:Jackmcbarn
   [5] https://en.wikipedia.org/wiki/Module:Testcase_rows
   [6] https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
   [7] https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License
]]

-- Load required modules
local yesno = require('Module:Yesno')

-- Set constants
local DATA_MODULE = 'Module:Template test case/data'

-------------------------------------------------------------------------------
-- Shared methods
-------------------------------------------------------------------------------

local function message(self, key, ...)
	-- This method is added to classes that need to deal with messages from the
	-- config module.
	local msg = self.cfg.msg[key]
	if select(1, ...) then
		return mw.message.newRawMessage(msg, ...):plain()
	else
		return msg
	end
end

-------------------------------------------------------------------------------
-- Template class
-------------------------------------------------------------------------------

local Template = {}

Template.memoizedMethods = {
	-- Names of methods to be memoized in each object. This table should only
	-- hold methods with no parameters.
	getFullPage = true,
	getName = true,
	makeHeader = true,
	getOutput = true
}

function Template.new(invocationObj, options)
	local obj = {}

	-- Set input
	for k, v in pairs(options or {}) do
		if not Template[k] then
			obj[k] = v
		end
	end
	obj._invocation = invocationObj

	-- Validate input
	if not obj.template and not obj.title then
		error('no template or title specified', 2)
	end

	-- Memoize expensive method calls
	local memoFuncs = {}
	return setmetatable(obj, {
		__index = function (t, key)
			if Template.memoizedMethods[key] then
				local func = memoFuncs[key]
				if not func then
					local val = Template[key](t)
					func = function () return val end
					memoFuncs[key] = func
				end
				return func
			else
				return Template[key]
			end
		end
	})
end

function Template:getFullPage()
	if not self.template then
		return self.title.prefixedText
	elseif self.template:sub(1, 7) == '#invoke' then
		return 'Module' .. self.template:sub(8):gsub('|.*', '')
	else
		local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
		hasColon = hasColon > 0
		local ns = strippedTemplate:match('^(.-):')
		ns = ns and mw.site.namespaces[ns]
		if ns then
			return strippedTemplate
		elseif hasColon then
			return strippedTemplate -- Main namespace
		else
			return mw.site.namespaces[10].name .. ':' .. strippedTemplate
		end
	end
end

function Template:getName()
	if self.template then
		return self.template
	else
		return require('Module:Template invocation').name(self.title)
	end
end

function Template:makeLink(display)
	if display then
		return string.format('[[:%s|%s]]', self:getFullPage(), display)
	else
		return string.format('[[:%s]]', self:getFullPage())
	end
end

function Template:makeBraceLink(display)
	display = display or self:getName()
	local link = self:makeLink(display)
	return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}')
end

function Template:makeHeader()
	return self.heading or self:makeBraceLink()
end

function Template:getInvocation(format)
	local invocation = self._invocation:getInvocation{
		template = self:getName(),
		requireMagicWord = self.requireMagicWord,
	}
	if format == 'code' then
		invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>'
	elseif format == 'kbd' then
		invocation = '<kbd>' .. mw.text.nowiki(invocation) .. '</kbd>'
	elseif format == 'plain' then
		invocation = mw.text.nowiki(invocation)
	else
		-- Default is pre tags
		invocation = mw.text.encode(invocation, '&')
		invocation = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>'
		invocation = mw.getCurrentFrame():preprocess(invocation)
	end
	return invocation
end

function Template:getOutput()
	local protect = require('Module:Protect')
	-- calling self._invocation:getOutput{...}
	return protect(self._invocation.getOutput)(self._invocation, {
		template = self:getName(),
		requireMagicWord = self.requireMagicWord,
	})
end

-------------------------------------------------------------------------------
-- TestCase class
-------------------------------------------------------------------------------

local TestCase = {}
TestCase.__index = TestCase
TestCase.message = message -- add the message method

TestCase.renderMethods = {
	-- Keys in this table are values of the "format" option, values are the
	-- method for rendering that format.
	columns = 'renderColumns',
	rows = 'renderRows',
	tablerows = 'renderRows',
	inline = 'renderInline',
	cells = 'renderCells',
	default = 'renderDefault'
}

function TestCase.new(invocationObj, options, cfg)
	local obj = setmetatable({}, TestCase)
	obj.cfg = cfg

	-- Separate general options from template options. Template options are
	-- numbered, whereas general options are not.
	local generalOptions, templateOptions = {}, {}
	for k, v in pairs(options) do
		local prefix, num
		if type(k) == 'string' then
			prefix, num = k:match('^(.-)([1-9][0-9]*)$')
		end
		if prefix then
			num = tonumber(num)
			templateOptions[num] = templateOptions[num] or {}
			templateOptions[num][prefix] = v
		else
			generalOptions[k] = v
		end
	end

	-- Set general options
	generalOptions.showcode = yesno(generalOptions.showcode)
	generalOptions.showheader = yesno(generalOptions.showheader) ~= false
	generalOptions.showcaption = yesno(generalOptions.showcaption) ~= false
	generalOptions.collapsible = yesno(generalOptions.collapsible)
	generalOptions.notcollapsed = yesno(generalOptions.notcollapsed)
	generalOptions.wantdiff = yesno(generalOptions.wantdiff) 
	obj.options = generalOptions

	-- Preprocess template args
	for num, t in pairs(templateOptions) do
		if t.showtemplate ~= nil then
			t.showtemplate = yesno(t.showtemplate)
		end
	end

	-- Set up first two template options tables, so that if only the
	-- "template3" is specified it isn't made the first template when the
	-- the table options array is compressed.
	templateOptions[1] = templateOptions[1] or {}
	templateOptions[2] = templateOptions[2] or {}

	-- Allow the "template" option to override the "template1" option for
	-- backwards compatibility with [[Module:Testcase table]].
	if generalOptions.template then
		templateOptions[1].template = generalOptions.template
	end

	-- Add default template options
	if templateOptions[1].template and not templateOptions[2].template then
		templateOptions[2].template = templateOptions[1].template ..
			'/' .. obj.cfg.sandboxSubpage
	end
	if not templateOptions[1].template then
		templateOptions[1].title = mw.title.getCurrentTitle().basePageTitle
	end
	if not templateOptions[2].template then
		templateOptions[2].title = templateOptions[1].title:subPageTitle(
			obj.cfg.sandboxSubpage
		)
	end

	-- Remove template options for any templates where the showtemplate
	-- argument is false. This prevents any output for that template.
	for num, t in pairs(templateOptions) do
		if t.showtemplate == false then
			templateOptions[num] = nil
		end
	end

	-- Check for missing template names.
	for num, t in pairs(templateOptions) do
		if not t.template and not t.title then
			error(obj:message(
				'missing-template-option-error',
				num, num
			), 2)
		end
	end

	-- Compress templateOptions table so we can iterate over it with ipairs.
	templateOptions = (function (t)
		local nums = {}
		for num in pairs(t) do
			nums[#nums + 1] = num
		end
		table.sort(nums)
		local ret = {}
		for i, num in ipairs(nums) do
			ret[i] = t[num]
		end
		return ret
	end)(templateOptions)

	-- Don't require the __TEMPLATENAME__ magic word for nowiki invocations if
	-- there is only one template being output.
	if #templateOptions <= 1 then
		templateOptions[1].requireMagicWord = false
	end

	mw.logObject(templateOptions)

	-- Make the template objects
	obj.templates = {}
	for i, options in ipairs(templateOptions) do
		table.insert(obj.templates, Template.new(invocationObj, options))
	end

	-- Add tracking categories. At the moment we are only tracking templates
	-- that use any "heading" parameters or an "output" parameter.
	obj.categories = {}
	for k, v in pairs(options) do
		if type(k) == 'string' and k:find('heading') then
			obj.categories['Test cases using heading parameters'] = true
		elseif k == 'output' then
			obj.categories['Test cases using output parameter'] = true
		end
	end

	return obj
end

function TestCase:getTemplateOutput(templateObj)
	local output = templateObj:getOutput()
	if self.options.resetRefs then
		mw.getCurrentFrame():extensionTag('references')
	end
	return output
end

function TestCase:templateOutputIsEqual()
	-- Returns a boolean showing whether all of the template outputs are equal.
	-- The random parts of strip markers (see [[Help:Strip markers]]) are
	-- removed before comparison. This means a strip marker can contain anything
	-- and still be treated as equal, but it solves the problem of otherwise
	-- identical wikitext not returning as exactly equal.
	local function normaliseOutput(obj)
		local out = obj:getOutput()
		-- Remove the random parts from strip markers.
		out = out:gsub('(\127[^\127]*UNIQ%-%-%l+%-)%x+(%-%-?QINU[^\127]*\127)', '%1%2')
		return out
	end
	local firstOutput = normaliseOutput(self.templates[1])
	for i = 2, #self.templates do
		local output = normaliseOutput(self.templates[i])
		if output ~= firstOutput then
			return false
		end
	end
	return true
end

function TestCase:makeCollapsible(s)
	local title = self.options.title or self.templates[1]:makeHeader()
	if self.options.titlecode then
		title = self.templates[1]:getInvocation('kbd')
	end
	local isEqual = self:templateOutputIsEqual()
	local root = mw.html.create('div')
	root
		:addClass('mw-collapsible')
		:css('width', '100%')
		:css('border', 'solid silver 1px')
		:css('padding', '0.2em')
		:css('clear', 'both')
		:addClass(self.options.notcollapsed == false and 'mw-collapsed' or nil)
	if self.options.wantdiff then
		root
			:tag('div')
				:css('background-color', isEqual and 'yellow' or '#90a8ee')
				:css('color', 'black')
				:css('font-weight', 'bold')
				:css('padding', '0.2em')
				:wikitext(title)
				:done()
	else
		if self.options.notcollapsed ~= true or false then
			root
				:addClass(isEqual and 'mw-collapsed' or nil)
		end
		root
			:tag('div')
				:css('background-color', isEqual and 'lightgreen' or 'yellow')
				:css('color', 'black')
				:css('font-weight', 'bold')
				:css('padding', '0.2em')
				:wikitext(title)
				:done()
	end
	root
		:tag('div')
			:addClass('mw-collapsible-content')
			:newline()
			:wikitext(s)
			:newline()
	return tostring(root)
end

function TestCase:renderColumns()
	local root = mw.html.create()
	if self.options.showcode then
		root
			:wikitext(self.templates[1]:getInvocation())
			:newline()
	end

	local tableroot = root:tag('table')

	if self.options.showheader then
		-- Caption
		if self.options.showcaption then
			tableroot
				:addClass(self.options.class)
				:cssText(self.options.style)
				:tag('caption')
					:wikitext(self.options.caption or self:message('columns-header'))
		end

		-- Headers
		local headerRow = tableroot:tag('tr')
		if self.options.rowheader then
			-- rowheader is correct here. We need to add another th cell if
			-- rowheader is set further down, even if heading0 is missing.
			headerRow:tag('th'):wikitext(self.options.heading0)
		end
		local width
		if #self.templates > 0 then
			width = tostring(math.floor(100 / #self.templates)) .. '%'
		else
			width = '100%'
		end
		for i, obj in ipairs(self.templates) do
			headerRow
				:tag('th')
					:css('width', width)
					:wikitext(obj:makeHeader())
		end
	end

	-- Row header
	local dataRow = tableroot:tag('tr'):css('vertical-align', 'top')
	if self.options.rowheader then
		dataRow:tag('th')
			:attr('scope', 'row')
			:wikitext(self.options.rowheader)
	end
	
	-- Template output
	for i, obj in ipairs(self.templates) do
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
				:wikitext('<pre style="white-space: pre-wrap;">')
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
				:wikitext('</pre>')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
				:newline()
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
		else
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
		end
	end
	
	return tostring(root)
end

function TestCase:renderRows()
	local root = mw.html.create()
	if self.options.showcode then
		root
			:wikitext(self.templates[1]:getInvocation())
			:newline()
	end

	local tableroot = root:tag('table')
	tableroot
		:addClass(self.options.class)
		:cssText(self.options.style)

	if self.options.caption then
		tableroot
			:tag('caption')
				:wikitext(self.options.caption)
	end

	for _, obj in ipairs(self.templates) do
		local dataRow = tableroot:tag('tr')
		
		-- Header
		if self.options.showheader then
			if self.options.format == 'tablerows' then
				dataRow:tag('th')
					:attr('scope', 'row')
					:css('vertical-align', 'top')
					:css('text-align', 'left')
					:wikitext(obj:makeHeader())
				dataRow:tag('td')
					:css('vertical-align', 'top')
					:css('padding', '0 1em')
					:wikitext('→')
			else
				dataRow:tag('td')
					:css('text-align', 'center')
					:css('font-weight', 'bold')
					:wikitext(obj:makeHeader())
				dataRow = tableroot:tag('tr')
			end
		end
		
		-- Template output
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
				:newline()
                :wikitext(self.options.before)
                :wikitext(self:getTemplateOutput(obj))
                :wikitext(self.options.after)
                :wikitext('<pre style="white-space: pre-wrap;">')
                :wikitext(mw.text.nowiki(self.options.before or ""))
                :wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
                :wikitext(mw.text.nowiki(self.options.after or ""))
                :wikitext('</pre>')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
				:newline()
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
		else
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
		end
	end

	return tostring(root)
end

function TestCase:renderInline()
	local arrow = mw.language.getContentLanguage():getArrow('forwards')
	local ret = {}
	for i, obj in ipairs(self.templates) do
		local line = {}
		line[#line + 1] = self.options.prefix or '* '
		if self.options.showcode then
			line[#line + 1] = obj:getInvocation('code')
			line[#line + 1] = ' '
			line[#line + 1] = arrow
			line[#line + 1] = ' '
		end
		if self.options.output == 'nowiki+' then
			line[#line + 1] = self.options.before or ""
			line[#line + 1] = self:getTemplateOutput(obj)
			line[#line + 1] = self.options.after or ""
			line[#line + 1] = '<pre style="white-space: pre-wrap;">'
			line[#line + 1] = mw.text.nowiki(self.options.before or "")
			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
			line[#line + 1] = mw.text.nowiki(self.options.after or "")
			line[#line + 1] = '</pre>'
		elseif self.options.output == 'nowiki' then
			line[#line + 1] = mw.text.nowiki(self.options.before or "")
			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
			line[#line + 1] = mw.text.nowiki(self.options.after or "")
		else
			line[#line + 1] = self.options.before or ""
			line[#line + 1] = self:getTemplateOutput(obj)
			line[#line + 1] = self.options.after or ""
		end
		ret[#ret + 1] = table.concat(line)
	end
	if self.options.addline then
		local line = {}
		line[#line + 1] = self.options.prefix or '* '
		line[#line + 1] = self.options.addline
		ret[#ret + 1] = table.concat(line)
	end
	return table.concat(ret, '\n')
end

function TestCase:renderCells()
	local root = mw.html.create()
	local dataRow = root:tag('tr')
	dataRow
		:css('vertical-align', 'top')
		:addClass(self.options.class)
		:cssText(self.options.style)

	-- Row header
	if self.options.rowheader then
		dataRow:tag('th')
			:attr('scope', 'row')
			:newline()
			:wikitext(self.options.rowheader or self:message('row-header'))
	end
	-- Caption
	if self.options.showcaption then
		dataRow:tag('th')
			:attr('scope', 'row')
			:newline()
			:wikitext(self.options.caption or self:message('columns-header'))
	end

	-- Show code
	if self.options.showcode then
		dataRow:tag('td')
			:newline()
			:wikitext(self:getInvocation('code'))
	end

	-- Template output
	for i, obj in ipairs(self.templates) do
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
				:wikitext('<pre style="white-space: pre-wrap;">')
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
				:wikitext('</pre>')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
				:newline()
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
		else
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
		end
	end

	return tostring(root)
end

function TestCase:renderDefault()
	local ret = {}
	if self.options.showcode then
		ret[#ret + 1] = self.templates[1]:getInvocation()
	end
	for i, obj in ipairs(self.templates) do
		ret[#ret + 1] = '<div style="clear: both;"></div>'
		if self.options.showheader then
			ret[#ret + 1] = obj:makeHeader()
		end
		if self.options.output == 'nowiki+' then
			ret[#ret + 1] = (self.options.before or "") ..
			self:getTemplateOutput(obj) ..
			(self.options.after or "") ..
			'<pre style="white-space: pre-wrap;">' ..
			mw.text.nowiki(self.options.before or "") ..
			mw.text.nowiki(self:getTemplateOutput(obj)) ..
			mw.text.nowiki(self.options.after or "") .. '</pre>'
		elseif self.options.output == 'nowiki' then
			ret[#ret + 1] = mw.text.nowiki(self.options.before or "") ..
			mw.text.nowiki(self:getTemplateOutput(obj)) ..
			mw.text.nowiki(self.options.after or "")
		else
			ret[#ret + 1] = (self.options.before or "") ..
			self:getTemplateOutput(obj) ..
			(self.options.after or "")
		end
	end
	return table.concat(ret, '\n\n')
end

function TestCase:__tostring()
	local format = self.options.format
	local method = format and TestCase.renderMethods[format] or 'renderDefault'
	local ret = self[method](self)
	if self.options.collapsible then
		ret = self:makeCollapsible(ret)
	end
	for cat in pairs(self.categories) do
		ret = ret .. string.format('[[Category:%s]]', cat)
	end
	return ret
end

-------------------------------------------------------------------------------
-- Nowiki invocation class
-------------------------------------------------------------------------------

local NowikiInvocation = {}
NowikiInvocation.__index = NowikiInvocation
NowikiInvocation.message = message -- Add the message method

function NowikiInvocation.new(invocation, cfg)
	local obj = setmetatable({}, NowikiInvocation)
	obj.cfg = cfg
	invocation = mw.text.unstrip(invocation)
	-- Decode HTML entities for <, >, and ". This means that HTML entities in
	-- the original code must be escaped as e.g. &amp;lt;, which is unfortunate,
	-- but it is the best we can do as the distinction between <, >, " and &lt;,
	-- &gt;, &quot; is lost during the original nowiki operation.
	invocation = invocation:gsub('&lt;', '<')
	invocation = invocation:gsub('&gt;', '>')
	invocation = invocation:gsub('&quot;', '"')
	obj.invocation = invocation
	return obj
end

function NowikiInvocation:getInvocation(options)
	local template = options.template:gsub('%%', '%%%%') -- Escape "%" with "%%"
	local invocation, count = self.invocation:gsub(
		self.cfg.templateNameMagicWordPattern,
		template
	)
	if options.requireMagicWord ~= false and count < 1 then
		error(self:message(
			'nowiki-magic-word-error',
			self.cfg.templateNameMagicWord
		))
	end
	return invocation
end

function NowikiInvocation:getOutput(options)
	local invocation = self:getInvocation(options)
	return mw.getCurrentFrame():preprocess(invocation)
end

-------------------------------------------------------------------------------
-- Table invocation class
-------------------------------------------------------------------------------

local TableInvocation = {}
TableInvocation.__index = TableInvocation
TableInvocation.message = message -- Add the message method

function TableInvocation.new(invokeArgs, nowikiCode, cfg)
	local obj = setmetatable({}, TableInvocation)
	obj.cfg = cfg
	obj.invokeArgs = invokeArgs
	obj.code = nowikiCode
	return obj
end

function TableInvocation:getInvocation(options)
	if self.code then
		local nowikiObj = NowikiInvocation.new(self.code, self.cfg)
		return nowikiObj:getInvocation(options)
	else
		return require('Module:Template invocation').invocation(
			options.template,
			self.invokeArgs
		)
	end
end

function TableInvocation:getOutput(options)
	if (options.template:sub(1, 7) == '#invoke') then
		local moduleCall = mw.text.split(options.template, '|', true)
		local args = mw.clone(self.invokeArgs)
		table.insert(args, 1, moduleCall[2])
		return mw.getCurrentFrame():callParserFunction(moduleCall[1], args)
	end
	return mw.getCurrentFrame():expandTemplate{
		title = options.template,
		args = self.invokeArgs
	}
end

-------------------------------------------------------------------------------
-- Bridge functions
--
-- These functions translate template arguments into forms that can be accepted
-- by the different classes, and return the results.
-------------------------------------------------------------------------------

local bridge = {}

function bridge.table(args, cfg)
	cfg = cfg or mw.loadData(DATA_MODULE)

	local options, invokeArgs = {}, {}
	for k, v in pairs(args) do
		local optionKey = type(k) == 'string' and k:match('^_(.*)$')
		if optionKey then
			if type(v) == 'string' then
				v = v:match('^%s*(.-)%s*$') -- trim whitespace
			end
			if v ~= '' then
				options[optionKey] = v
			end
		else
			invokeArgs[k] = v
		end
	end

	-- Allow passing a nowiki invocation as an option. While this means users
	-- have to pass in the code twice, whitespace is preserved and &lt; etc.
	-- will work as intended.
	local nowikiCode = options.code
	options.code = nil

	local invocationObj = TableInvocation.new(invokeArgs, nowikiCode, cfg)
	local testCaseObj = TestCase.new(invocationObj, options, cfg)
	return tostring(testCaseObj)
end

function bridge.nowiki(args, cfg)
	cfg = cfg or mw.loadData(DATA_MODULE)
	
	-- Convert args beginning with _ for consistency with the normal bridge
	local newArgs = {}
	for k, v in pairs(args) do
		local normalName = type(k) == "string" and string.match(k, "^_(.*)$")
		if normalName then
			newArgs[normalName] = v
		else
			newArgs[k] = v
		end
	end

	local code = newArgs.code or newArgs[1]
	local invocationObj = NowikiInvocation.new(code, cfg)
	newArgs.code = nil
	newArgs[1] = nil
	-- Assume we want to see the code as we already passed it in.
	newArgs.showcode = newArgs.showcode or true
	local testCaseObj = TestCase.new(invocationObj, newArgs, cfg)
	return tostring(testCaseObj)
end

-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------

local p = {}

function p.main(frame, cfg)
	cfg = cfg or mw.loadData(DATA_MODULE)

	-- Load the wrapper config, if any.
	local wrapperConfig
	if frame.getParent then
		local title = frame:getParent():getTitle()
		local template = title:gsub(cfg.sandboxSubpagePattern, '')
		wrapperConfig = cfg.wrappers[template]
	end

	-- Work out the function we will call, use it to generate the config for
	-- Module:Arguments, and use Module:Arguments to find the arguments passed
	-- by the user.
	local func = wrapperConfig and wrapperConfig.func or 'table'
	local userArgs = require('Module:Arguments').getArgs(frame, {
		parentOnly = wrapperConfig,
		frameOnly = not wrapperConfig,
		trim = func ~= 'table',
		removeBlanks = func ~= 'table'
	})

	-- Get default args and build the args table. User-specified args overwrite
	-- default args.
	local defaultArgs = wrapperConfig and wrapperConfig.args or {}
	local args = {}
	for k, v in pairs(defaultArgs) do
		args[k] = v
	end
	for k, v in pairs(userArgs) do
		args[k] = v
	end

	return bridge[func](args, cfg)
end

function p._exportClasses() -- For testing
	return {
		Template = Template,
		TestCase = TestCase,
		NowikiInvocation = NowikiInvocation,
		TableInvocation = TableInvocation
	}
end

return p