Module:Template test case
Documentation for this module may be created at Module:Template test case/doc
-- This module provides several methods to generate test cases.
local yesno = require('Module:Yesno')
local mTableTools = require('Module:TableTools')
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local TEMPLATE_NAME_MAGIC_WORD = '__TEMPLATENAME__'
local TEMPLATE_NAME_MAGIC_WORD_ESCAPED = TEMPLATE_NAME_MAGIC_WORD:gsub('%p', '%%%0')
-------------------------------------------------------------------------------
-- 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,
makeHeading = 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 self.template then
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
else
return self.title.prefixedText
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:makeHeading()
return self.heading or self:makeBraceLink()
end
function Template:getInvocation(format)
local invocation = self._invocation:getInvocation(self:getName())
invocation = mw.text.nowiki(invocation)
if format == 'code' then
invocation = '<code>' .. invocation .. '</code>'
elseif format ~= 'plain' then
-- Default is pre tags
invocation = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>'
invocation = mw.getCurrentFrame():preprocess(invocation)
end
return invocation
end
function Template:getOutput()
return self._invocation:getOutput(self:getName())
end
-------------------------------------------------------------------------------
-- TestCase class
-------------------------------------------------------------------------------
local TestCase = {}
TestCase.__index = TestCase
function TestCase.new(invocationObj, options)
local obj = setmetatable({}, TestCase)
-- Validate options
do
local highestNum = 0
for k in pairs(options) do
if type(k) == 'string' then
local num = k:match('([1-9][0-9]*)$')
num = tonumber(num)
if num and num > highestNum then
highestNum = num
end
end
end
for i = 3, highestNum do
if not options['template' .. i] then
error(string.format(
"one or more options ending in '%d' were " ..
"detected, but no 'template%d' option was found",
i, i
), 2)
end
end
end
-- Separate general options from options for specific templates
local templateOptions = mTableTools.numData(options, true)
obj.options = templateOptions.other or {}
-- Normalize showcode option
obj.options.showcode = yesno(obj.options.showcode)
-- Add default template options
templateOptions[1] = templateOptions[1] or {}
templateOptions[2] = templateOptions[2] or {}
if templateOptions[1].template and not templateOptions[2].template then
templateOptions[2].template = templateOptions[1].template .. '/sandbox'
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('sandbox')
end
-- Make the template objects
obj.templates = {}
for i, t in ipairs(templateOptions) do
table.insert(obj.templates, Template.new(invocationObj, t))
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:renderColumns()
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)
:tag('caption')
:wikitext(self.options.caption or 'Side by side comparison')
-- Headings
local headingRow = 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.
headingRow: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
headingRow
:tag('th')
:css('width', width)
:wikitext(obj:makeHeading())
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
dataRow:tag('td')
:newline()
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
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
-- Build the row HTML
tableroot
:tag('tr')
:tag('td')
:css('text-align', 'center')
:css('font-weight', 'bold')
:wikitext(obj:makeHeading())
:done()
:done()
:tag('tr')
:tag('td')
:newline()
:wikitext(self:getTemplateOutput(obj))
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>'
ret[#ret + 1] = obj:makeBraceLink()
ret[#ret + 1] = self:getTemplateOutput(obj)
end
return table.concat(ret, '\n\n')
end
function TestCase:__tostring()
local methods = {
columns = 'renderColumns',
rows = 'renderRows'
}
local format = self.options.format
local method = format and methods[format] or 'renderDefault'
return self[method](self)
end
-------------------------------------------------------------------------------
-- Nowiki invocation class
-------------------------------------------------------------------------------
local NowikiInvocation = {}
NowikiInvocation.__index = NowikiInvocation
function NowikiInvocation.new(invocation)
local obj = setmetatable({}, NowikiInvocation)
obj.invocation = mw.text.unstrip(invocation)
return obj
end
function NowikiInvocation:getInvocation(template)
template = template:gsub('%%', '%%%%') -- Escape "%" with "%%"
local invocation, count = self.invocation:gsub(
TEMPLATE_NAME_MAGIC_WORD_ESCAPED,
template
)
if count < 1 then
error(string.format(
"the template invocation must include '%s' in place " ..
"of the template name",
TEMPLATE_NAME_MAGIC_WORD
))
end
return invocation
end
function NowikiInvocation:getOutput(template)
local invocation = self:getInvocation(template)
return mw.getCurrentFrame():preprocess(invocation)
end
-------------------------------------------------------------------------------
-- Table invocation class
-------------------------------------------------------------------------------
local TableInvocation = {}
TableInvocation.__index = TableInvocation
function TableInvocation.new(invokeArgs)
local obj = setmetatable({}, TableInvocation)
obj.invokeArgs = invokeArgs
return obj
end
function TableInvocation:getInvocation(template)
return require('Module:Template invocation').invocation(
template,
self.invokeArgs
)
end
function TableInvocation:getOutput(template)
return mw.getCurrentFrame():expandTemplate{
title = template,
args = self.invokeArgs
}
end
-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------
-- Table-based exports
local function getTableArgs(frame, wrappers)
return require('Module:Arguments').getArgs(frame, {
wrappers = wrappers,
trim = false,
removeBlanks = false
})
end
local p = {}
function p._table(args)
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
local invocationObj = TableInvocation.new(invokeArgs)
local testCaseObj = TestCase.new(invocationObj, options)
return tostring(testCaseObj)
end
function p.table(frame)
return p._table(getTableArgs(frame, 'Template:Test case from arguments'))
end
function p.columns(frame)
local args = getTableArgs(frame, 'Template:Testcase table')
args._format = 'columns'
return p._table(args)
end
function p.rows(frame)
local args = getTableArgs(frame, 'Template:Testcase rows')
args._format = 'rows'
return p._table(args)
end
-- Nowiki-based exports
function p._nowiki(args)
local invocationObj = NowikiInvocation.new(args.code)
args.code = nil
-- Assume we want to see the code as we already passed it in.
args.showcode = args.showcode or true
local testCaseObj = TestCase.new(invocationObj, args)
return tostring(testCaseObj)
end
function p.nowiki(frame)
local args = require('Module:Arguments').getArgs(frame, {
wrappers = 'Template:Test case from invocation'
})
return p._nowiki(args)
end
-- Exports for testing
function p._exportClasses()
return {
TestCase = TestCase,
Invocation = Invocation,
NowikiInvocation = NowikiInvocation,
TableInvocation = TableInvocation
}
end
return p