Xray's built-in Document Generator feature, and also the Xporter Jira app, allows you to export information from Jira issues and supported Jira plugins, including Xray, to Word, Excel, PDF, or other type of documents.
The layout of the output document can be customized using templates built in .DOCX (Microsoft Word), .XLSX (Microsoft Excel) or even .ODT (Open Office) files.
Templates are regular documents where certain placeholders (e.g., #{...}, ${...}, &{...}, %{...}) can be used to render some fields or to perform auxiliary operations.
The Xporter language syntax used within the document template allows you to:
Please check Xporter's documentation to learn more about Working with Templates, and the page that has more information about the mappings available to obtain Xray related data.
Using ${...} resolves the inner expression that can either refer to:
Whenever using %{...} the inner expression is assumed to be inline JavaScript that will be evaluated; the output text will be returned and rendered in the document, for example.
Xray's Document Generator (or Xporter app) engine will receive either a issue or a list of issues as input/incoming context, depending from where the document was exported from.
A given issue may have fields that are "simple" single-value attributes or that are "lists". These lists may be array of objects (e.g., labels, components) stored on the issue or may be virtual lists of related issues/entities (e.g., Preconditions on a Test, all the Test Runs in a Test Execution). It's possible to iterate on these lists and the inner context becomes the element being iterated.
Parent expression | Incoming context | Inner context | Syntax for accessing fields on the inner context element | Example of accessing a field on the inner context element |
---|---|---|---|---|
none (i.e., whenever exporting a single issue, that issue becomes the parent context) | the Jira issue (i.e., the one it was exported from) | the incoming context (i.e., the issue) | ${...} | ${Priority} |
&{for issues} (i.e., whenever exporting multiple issues, inside the "&{for issues}" loop the issue becomes the current context) | the chosen Jira issues (e.g., the ones selected for bulk export) | a issue from the list of the issues from the incoming context | ${...} | ${Priority} |
#{for j=LinksCount} (i.e., some fields have a corresponding "...Count" element that can serve to implement an iterator) | the Jira issue (i.e., the one it was exported from) | each of the issue links, one by one, on the issue where it was export from | ${Links[j]} | |
#{for components} | the Jira issue (i.e., the one it was exported from) | each of the components, one by one, from the issue where it was export from | ${Components[n]} | ${Components[n].Name) |
#{for testruns} | the Jira issue (i.e., the one it was exported from) | each of the Test Runs, one by one, from the Test or Test Execution issue where it was export from | ${TestRuns[n]} | ${TestRuns[n].Key) |
#{for idx=JQLIssuesCount|clause=key in testRequirements('${Tests[n].Key}')} | N/A | each issue, one by one, returned by the JQL Expression | ${JQLIssues[idx]} | ${JQLIssues[idx].Summary} |
#{for s=TestRuns[n].TestStepsCount} | ${TestRuns[n].TestSteps[s]} | each Test Step, one by one, of a given Test Run, from the Test or Test Execution issue where it was export from | ${TestRuns[n].TestSteps[s]} | ${TestRuns[n].TestSteps[s].Action} |
Fields, for example for the current issue in context, are case sensitive.
It's possible to create temporary/auxiliary variables using the ${set(...)}
function.
Variables may contain simple values (e.g., string, integer, float based); they cannot contain complex objects or list/array of objects.
/* as string */ ${set(myVar, "A")} /* as integer or float */ ${set(myVar, 0)} ${set(myVar, 1.2)} |
%{${myVar}} |
${set(myVar, 0)} ${set(myVar, %{${myVar} + 1})} |
${set(myVar, 'A')} ${set(myVar,%{'${myVar}'.concat('${JQLIssues[a].Key},')})} |
JavaScript can be used to render some text or can be used in some operations (e.g., for filtering iterations, to implement if conditionals).
Some notes follow:
%{var a='xpto'; var b='dummy';} |
%{...} always returns something, ultimately a boolean
In the following example, the rendered document will have "true<newline>bla".
%{var a='xpto';} bla |
%{var a='xpto';''}bla |
JavaScript can be used to perform some pre or post-processing on variables using the power of JavaScript syntax. Examples include:
|
In JavaScript expressions we can use local JavaScript variables or we can refer to an external variable defined by Xporter itself.
Local JavaScript variables can be printed from outside the JS expression where they were first defined in. This means that JS variables will be available in other JS expressions that may follow.
%{var a='xpto';a.toUpperCase()} The "a" variable defined in a previous JS statement has the value %{a}. |
XPTO The "a" variable defined in a previous JS statement has the value xpto. |
In JS expression we can refer to external variables, defined by Xporter using the ${set(varname,value)} syntax.
${set(outsideVar, 1) ${set(myName, " Sergio F. ")} %{${outsideVar}+2} %{'${myName}'.trim()} |
3 Sergio F. |
Regular, ad-hoc loops as you have in traditional languages (e.g., "do ... while", "while ... do") are not supported.
"For loops" are partially supported but are restricted for iterating well-known entities/fields that are list/array based.
It's not possible to make a regular, ad-hoc "for loop" like "for(n=3;n<10;n++)"; it's a current limitation.
In JavaScript expressions, we can use a standard JavaScript for loop but all loop must live within that statement; we cannot use one JS expression to start the JS "for" statement and then use another JS expression later to end that loop.
%{for (let i = 0; i < cars.length; i++) { } ... %{ } } |
Iterations use "for"; however, the syntax changes slightly depending on the context.
Whenever using #{for ...}, &{for ...} please make sure that you don't have any spaces before and after or else it may not be processed. |
Whenever iterating over the issues received as the input for the template, the for syntax uses a &.
Within the loop, the issue is not referred explicitly; we can simply use the ${field} notation to obtain that field for the current issue in the iteration.
&{for issues} ${Key}: ${Summary} &{end} |
The index variable by default is called "n". In this case, we cannot define a custom name for the index variable.
#{for testruns} ${TestRuns[n].Execution Status} #{end} #{for components} ${Components[n].Name} #{end} #{for Images} ${Images[n].Image} #{end} |
Several entities have an attribute like "xxxCount", having a corresponding attribute "xxx" storing an array of objects.
The index variable is defined explicitly.
The for loop syntax is different from what we would expect at first sight.
/* this loop iterates over the JQLIssues attribute on the current scope, using index variable "m" (0<=m<JQLIssuesCount). It's like considering that JQLIssuesCount represents the size of the array named "JQLIssues". */ #{for m=JQLIssuesCount|clause=component = '${Components[n].Name}'} ${JQLIssues[m].Key} #{end} /* this loop iterates over the ExecutionEvidences attribute of TestRuns[n] object, using index variable "d" (0<=d<ExecutionEvidencesCount). It's like considering that TestRuns[n].ExecutionEvidencesCount represents the size of the array named TestRuns[n].ExecutionEvidences. */ #{for d=TestRuns[n].ExecutionEvidencesCount} Id: ${TestRuns[n].ExecutionEvidences[d].Id} #{end} |
/* iterate only over "Test Execution" issues */ &{for issues|filter=%{'${IssueTypeName}'.equals('Test Execution')}} ${Key} &{end} /* show issue keys of the linked issues using the "duplicates" issue link #{for j=LinksCount|filter=%{'${Links[j].LinkType}'.equals('duplicates')}} ${Links[j].Key} #{end} |
Conditionals use the "#{if ...}" syntax.
More info here.
/* compare temporary integer variable */ #{if (%{${total} <= 0})} #{end} /* compare integer variable/attribute */ #{if (%{'${IssueTypeName}'.equals('Test Execution') })} #{end} /* compare variable/attribute with a string */ #{if (%{'${Priority}'.equals('Medium')})} #{end} /* check based on boolean attribute */ #{if (%{${Comments[n].Internal}})} #{end} /* check if a field or a variable contain some substring */ #{if (%{'${desc}'.contains('some_word')})} #{end} |
Unfortunately, there is no support for "else" statement.
Thus, we need to implement by hand. We can either use another if statement that negates the first one or use a variable to achieve a similar purpose but in a more elegant way.
/* compare temporary integer variable */ #{if (%{(${total} <= 0)})} #{end} #{if (%{!(${total} <= 0)})} #{end} ${set(myCondition, %{${total} <= 0})} #{if (%{myCondition})} #{end} #{if (%{!myCondition})} #{end} |
Some fields may be list based, such as the Labels or the Components fields on issues. If we print them, they will be formatted such as "value1, value". In thse cases, we may want to remove the spaces, for example right after the comma.
%{'${TestRuns[n].Labels}'.replace(/\s/g, '')} |
Whenever joining elements by hand, in an iteration for example, we may concatenate strings appending a final delimeter (e.g., comma), leading to a results such as "<value1>,<value2>,". In this case it may be useful to remove the last comma and any remaining spaces.
%{'${defects}'.replace(/,\s*$/, '')} |
The following examples assume the current context is a Test issue.
${set(Req,'')} #{for a=JQLIssuesCount|clause=key in testRequirements('${Key}')} #{if (%{'${Req}'.indexOf('${Key},')==-1})} ${set(Req,%{'${Req}'.concat('${JQLIssues[a].Key},')})} #{end} #{end} %{'${Req}'.replace(/,\s*$/, '')} |
Thie example assumes that the coverage of a "requirement"/story by the related Test issues is made using the issue link "tests".
${set(Req,'')} #{for k=TestRuns[n].LinksCount|filter=%{'${TestRuns[n].Links[k].LinkType}'.equals('tests')}} #{if (%{'${Req}'.indexOf('${TestRuns[n].Links[k].Key}')==-1})} ${set(Req,%{'${Req}'.concat('${TestRuns[n].Links[k].Key},')})} #{end} #{end} %{'${Req}'.replace(/,\s*$/, '')}" |
#{for testruns} ${TestRuns[n].Execution Status} #{end} |
#{for preconditions} ${PreConditions[n].Key} ${PreConditions[n].Summary} ${PreConditions[n].Description} #{end} |
#{for l=TestRuns[n].PreConditionsCount} #{if (%{'${TestRuns[n].PreConditions[l].PreCondition.Definition}'.equals('')})} This Precondition ${TestRuns[n].PreConditions[l].Key} doesn’t have a definition. #{end} #{if (%{!'${TestRuns[n].PreConditions[l].PreCondition.Definition}'.equals('')})} Definition of Precondition ${TestRuns[n].PreConditions[l].Key}: ${TestRuns[n].PreConditions[l].PreCondition.Definition} #{end} |
#{for a=TestRuns[n].PreConditionsCount} @{title=${TestRuns[n].PreConditions[a].Key}|href=${BaseURL}/browse/${TestRuns[n].PreConditions[a].Key}} ${wiki:TestRuns[n].PreConditions[a].Conditions} #{end} |
#{for testSets} ${TestSets[n].Key} ${TestSets[n].Summary} ${TestSets[n].Description} #{end} |
#{for testExecutions} ${TestExecutions[n].Key} ${TestExecutions[n].Summary} ${TestExecutions[n].Description} #{end} |
#{for testPlans} ${TestPlans[n].Key} ${TestPlans[n].Summary} ${TestPlans[n].Description} #{end} |
The following examples assume the current context is a Test Plan.
/* Define a variable that will hold the keys of the requirements already listed */ ${set(Req, “A”)} /* iterate over all the Tests */ #{for tests} /* Fetch all Requirements for a given Test */ #{for a=JQLIssuesCount|clause=key in testRequirements('${Tests[n].Key}')} /* Check if Requirement key is in the variable */ #{if (%{'${Req}'.indexOf('${JQLIssues[a].Key}')==-1})} /* Add Requirement key to the variable */ ${set(Req,%{'${Req}'.concat('${JQLIssues[a].Key},')})} /* List URL, Summary, Status and Requirement Status */ @{title=${JQLIssues[a].Key}|href=${BaseURL}/browse/${JQLIssues[a].Key}} ${JQLIssues[a].Summary} ${JQLIssues[a].Status} ${JQLIssues[a].Requirement Status} /* Close if */ #{end} #{end} # /* Close for testRequirements */ #{end} |
${set(Req, ‘A’)} #{for tests} #{for k=Tests[n].LinksCount|filter=%{'${Tests[n].Links[k].LinkType}'.equals('tests')}} #{if (%{'${Req}'.indexOf('${Tests[n].Links[k].Key}')==-1})} ${set(Req,%{'${Req}'.concat('${Tests[n].Links[k].Key}')})} @{title=${Tests[n].Links[k].Key}|href=${BaseURL}/browse/${Tests[n].Links[k].Key}} ${Tests[n].Links[k].Summary} ${Tests[n].Links[k].Status} ${Tests[n].Links[k].Requirement Status} #{end} #{end} #{end} |
${set(Def, ‘A’)} #{for testExecutions} #{for a=TestExecutions[n].TestRunsCount} #{for d=TestExecutions[n].TestRuns[a].ExecutionDefectsCount} #{if (%{'${Def}'.indexOf('${TestExecutions[n].TestRuns[a].ExecutionDefects[d].Key}')==-1})} ${set(Def,%{'${Def}'.concat('${TestExecutions[n].TestRuns[a].ExecutionDefects[d].Key}')})} @{title=${TestExecutions[n].TestRuns[a].ExecutionDefects[d].Key}|href=${BaseURL}/browse/${TestExecutions[n].TestRuns[a].ExecutionDefects[d].Key}} ${TestExecutions[n].TestRuns[a].ExecutionDefects[d].Summary} #{end} #{end} #{for m=TestExecutions[n].TestRuns[a].TestStepsCount} #{for dc=TestExecutions[n].TestRuns[a].TestSteps[m].DefectsCount} #{if (%{'${Def}'.indexOf('${TestExecutions[n].TestRuns[a].TestSteps[m].Defects[dc].Key}')==-1})} ${set(Def,%{'${Def}'.concat('${TestExecutions[n].TestRuns[a].TestSteps[m].Defects[dc].Key}')})} ${TestExecutions[n].TestRuns[a].TestSteps[m].Defects[dc].Key} ${TestExecutions[n].TestRuns[a].TestSteps[m].Defects[dc].Summary} #{end} #{end} #{end} #{for it=TestExecutions[n].TestRuns[a].IterationsCount} #{for r=TestExecutions[n].TestRuns[a].Iterations[it].TestStepsCount} #{for dc=TestExecutions[n].TestRuns[a].Iterations[it].TestSteps[r].DefectsCount} #{if (%{'${Def}'.indexOf('${TestExecutions[n].TestRuns[a].Iterations[it].TestSteps[r].Defects[dc].Key}')==-1})} ${set(Def,%{'${Def}'.concat('${TestExecutions[n].TestRuns[a].Iterations[it].TestSteps[r].Defects[dc].Key}')})} @{title=${TestExecutions[n].TestRuns[a].Iterations[it].TestSteps[r].Defects[dc].Key}|href=${BaseURL}/browse/${TestExecutions[n].TestRuns[a].Iterations[it].TestSteps[r].Defects[dc].Key}} ${TestExecutions[n].TestRuns[a].Iterations[it].TestSteps[r].Defects[dc].Summary} #{end} #{end} #{end} #{end} #{end} #{end} |
Notes:
#{for j=TestExecutionsCount} ${TestExecutions[j].Key} #{end} #{for testExecutions} ${TestExecutions[n].Key} #{end} #{for j=JQLIssuesCount|clause= key in testPlanTestExecutions('${Key}')} ${JQLIssues[j].Key} #{end} |
${set(Req, "")} #{for a=JQLIssuesCount|clause=key in testRequirements('${TestRuns[n]}')} #{if (%{"${Req}".indexOf("${TestRuns[n].Key},")==-1})} ${set(Req,%{"${Req}".concat(”${JQLIssues[a].Key},")})} #{end} #{end} %{'${Req}'.replace(/,\s*$/, '')}" |
${set(Req,'')} #{for k=TestRuns[n].LinksCount|filter=%{'${TestRuns[n].Links[k].LinkType}'.equals('tests')}} #{if (%{'${Req}'.indexOf('${TestRuns[n].Links[k].Key}')==-1})} ${set(Req,%{'${Req}'.concat('${TestRuns[n].Links[k].Key},')})} #{end} #{end} %{'${Req}'.replace(/,\s*$/, '')}" |
See the example below for "Total Defects Count linked to a given Test Run" and use the defects variable at the end instead.
Notes:
${set(defects, "")} #{if (%{!${TestRuns[n].IsDataDriven}})} ${set(totalDefects,%{${TestRuns[n].ExecutionDefectsCount} + ${TestRuns[n].TestStepsDefectsCount} })} #{for e=TestRuns[n].ExecutionDefectsCount} ${set(defects, %{'${defects}'.concat('${TestRuns[n].ExecutionDefects[e].Key},')})} #{end} #{for s=TestRuns[n].TestStepsCount} #{for d=TestRuns[n].TestSteps[s].DefectsCount} #{if (%{'${defects}'.indexOf('${TestRuns[n].TestSteps[s].Defects[d].Key},') == -1})} ${set(defects, %{'${defects}'.concat('${TestRuns[n].TestSteps[s].Defects[d].Key},')})} ${set(totalDefects,%{${totalDefects}+1})} #{end} #{end} #{end} #{end} #{if (%{${TestRuns[n].IsDataDriven}})} ${set(totalDefects,%{${TestRuns[n].ExecutionDefectsCount}})} #{for m=TestRuns[n].IterationsCount} #{for e=TestRuns[n].ExecutionDefectsCount} #{if (%{'${defects}'.indexOf('${TestRuns[n].ExecutionDefects[e].Key},') == -1})} ${set(defects, %{'${defects}'.concat('${TestRuns[n].ExecutionDefects[e].Key},')})} #{end} #{end} #{for s=TestRuns[n].Iterations[m].TestStepsCount} #{for d=TestRuns[n].Iterations[m].TestSteps[s].DefectsCount} #{if (%{'${defects}'.indexOf('${TestRuns[n].Iterations[m].TestSteps[s].Defects[d].Key},') == -1})} ${set(defects, %{'${defects}'.concat('${TestRuns[n].Iterations[m].TestSteps[s].Defects[d].Key},')})} ${set(totalDefects,%{${totalDefects}+1})} #{end} #{end} #{end} #{end} #{end} ${set(defects, %{'${defects}'.replace(/,\s*$/, '')})} ${totalDefects} |
${set(defects, "")} #{if (%{!${Tests[j].TestRuns[n].IsDataDriven}})} ${set(totalDefects,%{${Tests[j].TestRuns[n].ExecutionDefectsCount} + ${Tests[j].TestRuns[n].TestStepsDefectsCount} })} #{for e=Tests[j].TestRuns[n].ExecutionDefectsCount} ${set(defects, %{'${defects}'.concat('${Tests[j].TestRuns[n].ExecutionDefects[e].Key},')})} #{end} #{for s=Tests[j].TestRuns[n].TestStepsCount} #{for d=Tests[j].TestRuns[n].TestSteps[s].DefectsCount} #{if (%{'${defects}'.indexOf('${Tests[j].TestRuns[n].TestSteps[s].Defects[d].Key},') == -1})} ${set(defects, %{'${defects}'.concat('${Tests[j].TestRuns[n].TestSteps[s].Defects[d].Key},')})} ${set(totalDefects,%{${totalDefects}+1})} #{end} #{end} #{end} #{end} #{if (%{${Tests[j].TestRuns[n].IsDataDriven}})} ${set(totalDefects,%{${Tests[j].TestRuns[n].ExecutionDefectsCount}})} #{for m=Tests[j].TestRuns[n].IterationsCount} #{for e=Tests[j].TestRuns[n].ExecutionDefectsCount} #{if (%{'${defects}'.indexOf('${Tests[j].TestRuns[n].ExecutionDefects[e].Key},') == -1})} ${set(defects, %{'${defects}'.concat('${Tests[j].TestRuns[n].ExecutionDefects[e].Key},')})} #{end} #{end} #{for s=Tests[j].TestRuns[n].Iterations[m].TestStepsCount} #{for d=Tests[j].TestRuns[n].Iterations[m].TestSteps[s].DefectsCount} #{if (%{'${defects}'.indexOf('${Tests[j].TestRuns[n].Iterations[m].TestSteps[s].Defects[d].Key},') == -1})} ${set(defects, %{'${defects}'.concat('${Tests[j].TestRuns[n].Iterations[m].TestSteps[s].Defects[d].Key},')})} ${set(totalDefects,%{${totalDefects}+1})} #{end} #{end} #{end} #{end} #{end} ${set(defects, %{'${defects}'.replace(/,\s*$/, '')})} ${totalDefects} |
Notes: