Module:Template test case: Difference between revisions

Content deleted Content added
move the render methods table to the base of the TestCase class
dark mode fix
 
(57 intermediate revisions by 14 users not shown)
Line 1:
--[[
-- This module provides several methods to generate test cases.
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')
local mTableTools = require('Module:TableTools')
 
-- 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
 
-------------------------------------------------------------------------------
Line 19 ⟶ 54:
getFullPage = true,
getName = true,
makeHeadingmakeHeader = true,
getOutput = true
}
Line 59 ⟶ 94:
 
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
Line 71 ⟶ 110:
return mw.site.namespaces[10].name .. ':' .. strippedTemplate
end
else
return self.title.prefixedText
end
end
Line 98 ⟶ 135:
end
 
function Template:makeHeadingmakeHeader()
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)
Line 118 ⟶ 160:
 
function Template:getOutput()
local protect = require('Module:Protect')
return self._invocation:getOutput(self:getName())
-- calling self._invocation:getOutput{...}
return protect(self._invocation.getOutput)(self._invocation, {
template = self:getName(),
requireMagicWord = self.requireMagicWord,
})
end
 
Line 127 ⟶ 174:
local TestCase = {}
TestCase.__index = TestCase
TestCase.message = message -- add the message method
 
TestCase.renderMethods = {
Line 133 ⟶ 181:
columns = 'renderColumns',
rows = 'renderRows',
tablerows = 'renderRows',
inline = 'renderInline',
cells = 'renderCells',
default = 'renderDefault'
}
Line 140 ⟶ 191:
obj.cfg = cfg
 
-- Separate general options from template options. Template options are
-- Validate options
-- numbered, whereas general options are not.
do
local generalOptions, templateOptions = {}, {}
local highestNum = 0
for k, v in pairs(options) do
local prefix, num
if type(k) == 'string' then
localprefix, num = k:match('^(.-)([1-9][0-9]*)$')
num = tonumber(num)
if num and num > highestNum then
highestNum = num
end
end
end
if prefix then
for i = 3, highestNum do
num = tonumber(num)
if not options['template' .. i] then
templateOptions[num] = templateOptions[num] or {}
error(string.format(
templateOptions[num][prefix] = v
"one or more options ending in '%d' were " ..
else
"detected, but no 'template%d' option was found",
generalOptions[k] = v
i, i
), 2)
end
end
end
 
-- SeparateSet general options from options for specific templates
generalOptions.showcode = yesno(generalOptions.showcode)
local templateOptions = mTableTools.numData(options, true)
generalOptions.showheader = yesno(generalOptions.showheader) ~= false
obj.options = templateOptions.other or {}
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
-- Normalize boolean options
for num, t in pairs(templateOptions) do
obj.options.showcode = yesno(obj.options.showcode)
if t.showtemplate ~= nil then
obj.options.collapsible = yesno(obj.options.collapsible)
t.showtemplate = yesno(t.showtemplate)
end
end
 
-- AddSet defaultup 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 .. '/sandbox'
'/' .. obj.cfg.sandboxSubpage
end
if not templateOptions[1].template then
Line 181 ⟶ 245:
end
if not templateOptions[2].template then
templateOptions[2].title = templateOptions[1].title:subPageTitle('sandbox')
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, toptions in ipairs(templateOptions) do
table.insert(obj.templates, Template.new(invocationObj, toptions))
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
 
Line 210 ⟶ 327:
local out = obj:getOutput()
-- Remove the random parts from strip markers.
out = out:gsub('(\127[^\127]*UNIQ%cUNIQ).-%-%l+%-)%x+(QINU%c-%-?QINU[^\127]*\127)', '%1%2')
return out
end
Line 224 ⟶ 341:
 
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('tablediv')
root
:wikitext(mw.getCurrentFrame():extensionTag{
:addClass('collapsible')
name = 'templatestyles',
:addClass(isEqual and 'collapsed' or nil)
args = { src = 'Module:Template test case/styles.css' },
:css('background-color', 'transparent')
})
:css('width', '100%')
:addClass('mw-collapsible')
:css('border', 'solid silver 1px')
:addClass('test-case-collapsible')
:tag('tr')
:addClass(self.options.notcollapsed == false and 'mw-collapsed' or nil)
:tag('th')
if self.options.wantdiff then
:css('background-color', isEqual and 'lightgreen' or 'yellow')
root
:wikitext(self.options.title or self.templates[1]:makeHeading())
:tag('div')
:addClass(isEqual and 'test-case-collapsible-b1' or 'test-case-collapsible-b2')
:wikitext(title)
:done()
else
:done()
if self.options.notcollapsed ~= true or false then
:tag('tr')
:tag('td')root
:addClass(isEqual and 'mw-collapsed' or nil)
:wikitext(s)
end
root
:tag('div')
:addClass(isEqual and 'test-case-collapsible-b3' or 'test-case-collapsible-b1')
:wikitext(title)
:done()
end
root
:tag('div')
:addClass('mw-collapsible-content')
:newline()
:wikitext(s)
:newline()
return tostring(root)
end
Line 253 ⟶ 390:
 
local tableroot = root:tag('table')
tableroot
:addClass(self.options.class)
:cssText(self.options.style)
:tag('caption')
:wikitext(self.options.caption or 'Side by side comparison')
 
if self.options.showheader then
-- Headings
-- Caption
local headingRow = tableroot:tag('tr')
if self.options.rowheadershowcaption then
tableroot
-- rowheader is correct here. We need to add another th cell if
:addClass(self.options.class)
-- rowheader is set further down, even if heading0 is missing.
headingRow :tag('th'):wikitextcssText(self.options.heading0style)
:tag('caption')
end
:wikitext(self.options.caption or self:message('columns-header'))
local width
end
if #self.templates > 0 then
 
width = tostring(math.floor(100 / #self.templates)) .. '%'
-- Headers
else
local headerRow = tableroot:tag('tr')
width = '100%'
if self.options.rowheader then
end
-- rowheader is correct here. We need to add another th cell if
for i, obj in ipairs(self.templates) do
-- rowheader is set further down, even if heading0 is missing.
headingRow
headerRow:tag('th'):wikitext(self.options.heading0)
end
:css('width', width)
local width
:wikitext(obj:makeHeading())
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
 
Line 289 ⟶ 432:
-- Template output
for i, obj in ipairs(self.templates) do
if self.options.output == 'nowiki+' then
dataRow:tag('td')
dataRow:newlinetag('td')
:newline()
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.afterbefore)
: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
Line 318 ⟶ 481:
 
for _, obj in ipairs(self.templates) do
local dataRow = tableroot:tag('tr')
-- Build the row HTML
tableroot
-- Header
:tag('tr')
if self.options.showheader then
:tag('td')
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:makeHeadingmakeHeader())
dataRow = tableroot:donetag('tr')
:done()end
end
:tag('tr')
:tag('td')
-- Template output
:newline()
if self.options.output == 'nowiki+' then
:wikitext(self:getTemplateOutput(obj))
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
 
Line 343 ⟶ 643:
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:makeBraceLink()
ret[#ret + 1] = selfobj:getTemplateOutputmakeHeader(obj)
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')
Line 355 ⟶ 673:
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
Line 365 ⟶ 686:
local NowikiInvocation = {}
NowikiInvocation.__index = NowikiInvocation
NowikiInvocation.message = message -- Add the message method
 
function NowikiInvocation.new(invocation, cfg)
Line 381 ⟶ 703:
end
 
function NowikiInvocation:getInvocation(templateoptions)
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(string.formatself:message(
'nowiki-magic-word-error',
"the template invocation must include '%s' in place " ..
"of the template name",
self.cfg.templateNameMagicWord
))
Line 397 ⟶ 718:
end
 
function NowikiInvocation:getOutput(templateoptions)
local invocation = self:getInvocation(templateoptions)
return mw.getCurrentFrame():preprocess(invocation)
end
Line 408 ⟶ 729:
local TableInvocation = {}
TableInvocation.__index = TableInvocation
TableInvocation.message = message -- Add the message method
 
function TableInvocation.new(invokeArgs, nowikiCode, cfg)
Line 417 ⟶ 739:
end
 
function TableInvocation:getInvocation(templateoptions)
if self.code then
local nowikiObj = NowikiInvocation.new(self.code, self.cfg)
return nowikiObj:getInvocation(templateoptions)
else
return require('Module:Template invocation').invocation(
options.template,
self.invokeArgs
)
Line 429 ⟶ 751:
end
 
function TableInvocation:getOutput(templateoptions)
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
}
Line 437 ⟶ 765:
 
-------------------------------------------------------------------------------
-- Bridge functions
-- Exports
--
-- These functions translate template arguments into forms that can be accepted
-- by the different classes, and return the results.
-------------------------------------------------------------------------------
 
local pbridge = {}
 
function pbridge.table(args, cfg)
cfg = cfg or mw.loadData(DATA_MODULE)
 
Line 471 ⟶ 802:
end
 
function pbridge.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 invocationObjcode = NowikiInvocation.new(argsnewArgs.code, cfg)or newArgs[1]
local invocationObj = NowikiInvocation.new(code, cfg)
args.code = nil
newArgs.code = nil
newArgs[1] = nil
-- Assume we want to see the code as we already passed it in.
argsnewArgs.showcode = argsnewArgs.showcode or true
local testCaseObj = TestCase.new(invocationObj, argsnewArgs, cfg)
return tostring(testCaseObj)
end
 
-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------
 
local p = {}
 
function p.main(frame, cfg)
Line 515 ⟶ 865:
end
 
return pbridge[func](args, cfg)
end