Add "with" expression for templating #53
Allows optionally rendering content if an argument is given. The expression is designed to be used with `optional` parameters. Goal is to allow using `RunPowerShell` function on every function that consists of PowerShell code. Before this commit, they were all required to provide revertCode, or none of them could be able to have it. It would not work because some scripts can be reverted, meanwhile some are one-way scripts that cannot be reverted (such as cleaning scripts). In this case a way to optionally render revertCode was required. `with` expression give each callee script ability to turn off `revertCode` if not needed, therefore enables using `RunPowerShell` everywhere. This commit also improves error message for script code for better debugging and refactors parser tests for more code reuse. It also adds more tests to parameter substitution, and renames some tests of both expressions for consistency.
This commit is contained in:
@@ -108,6 +108,7 @@
|
|||||||
#### Expressions
|
#### Expressions
|
||||||
|
|
||||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||||
|
- Expression syntax is inspired by [Go Templates](https://pkg.go.dev/text/template)
|
||||||
|
|
||||||
##### Parameter substitution
|
##### Parameter substitution
|
||||||
|
|
||||||
@@ -148,6 +149,25 @@ A function can call other functions such as:
|
|||||||
code: Hello {{ $argument }} !
|
code: Hello {{ $argument }} !
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### with
|
||||||
|
|
||||||
|
- Skips the block if the variable is absent or empty.
|
||||||
|
- Binds its context (`.`) value of provided argument for the parameter only if its value is provided.
|
||||||
|
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`
|
||||||
|
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: FunctionThatOutputsConditionally
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
$argument's value is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
#### `Function` syntax
|
#### `Function` syntax
|
||||||
|
|
||||||
- `name`: *`string`* (**required**)
|
- `name`: *`string`* (**required**)
|
||||||
@@ -188,6 +208,7 @@ A function can call other functions such as:
|
|||||||
- Otherwise it throws.
|
- Otherwise it throws.
|
||||||
- 💡 Set it to `true` if a parameter is used conditionally;
|
- 💡 Set it to `true` if a parameter is used conditionally;
|
||||||
- Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
|
- Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
|
||||||
|
- 💡 Can be used in conjunction with [`with` expression](#with).
|
||||||
|
|
||||||
### `ScriptingDefinition`
|
### `ScriptingDefinition`
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
import { IExpressionParser } from './IExpressionParser';
|
import { IExpressionParser } from './IExpressionParser';
|
||||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||||
|
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
|
new WithParser(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
|
||||||
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
|
||||||
|
export class WithParser extends RegexParser {
|
||||||
|
protected readonly regex = /{{\s*with\s+\$([^}| ]+)\s*}}\s*([^)]+?)\s*{{\s*end\s*}}/g;
|
||||||
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
|
const parameterName = match[1];
|
||||||
|
const innerText = match[2];
|
||||||
|
return {
|
||||||
|
parameters: [ new FunctionParameter(parameterName, true) ],
|
||||||
|
evaluator: (args) => {
|
||||||
|
const argumentValue = args.hasArgument(parameterName) ?
|
||||||
|
args.getArgument(parameterName).argumentValue
|
||||||
|
: undefined;
|
||||||
|
if (!argumentValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const substitutionRegex = /{{\s*.\s*}}/g;
|
||||||
|
const newText = innerText.replace(substitutionRegex, argumentValue);
|
||||||
|
return newText;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -389,7 +389,15 @@ actions:
|
|||||||
code: net user defaultuser0 /delete 2>nul
|
code: net user defaultuser0 /delete 2>nul
|
||||||
-
|
-
|
||||||
name: Empty trash bin
|
name: Empty trash bin
|
||||||
code: Powershell -Command "$bin = (New-Object -ComObject Shell.Application).NameSpace(10);$bin.items() | ForEach { Write-Host "Deleting $($_.Name) from Recycle Bin"; Remove-Item $_.Path -Recurse -Force}"
|
call:
|
||||||
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
|
code:
|
||||||
|
$bin = (New-Object -ComObject Shell.Application).NameSpace(10);
|
||||||
|
$bin.items() | ForEach {
|
||||||
|
Write-Host "Deleting $($_.Name) from Recycle Bin";
|
||||||
|
Remove-Item $_.Path -Recurse -Force
|
||||||
|
}
|
||||||
-
|
-
|
||||||
name: Enable Reset Base in Dism Component Store
|
name: Enable Reset Base in Dism Component Store
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3803,14 +3811,16 @@ actions:
|
|||||||
code: reg delete "HKCU\Environment" /v "OneDrive" /f
|
code: reg delete "HKCU\Environment" /v "OneDrive" /f
|
||||||
-
|
-
|
||||||
name: Uninstall Edge (chromium-based)
|
name: Uninstall Edge (chromium-based)
|
||||||
code:
|
call:
|
||||||
PowerShell -ExecutionPolicy Unrestricted -Command "
|
function: RunPowerShell
|
||||||
$installer = (Get-ChildItem \"$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe\");
|
parameters:
|
||||||
if (!$installer) {
|
code:
|
||||||
Write-Host Could not find the installer;
|
$installer = (Get-ChildItem \"$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe\");
|
||||||
} else {
|
if (!$installer) {
|
||||||
& $installer.FullName -uninstall -system-level -verbose-logging -force-uninstall
|
Write-Host Could not find the installer;
|
||||||
}; "
|
} else {
|
||||||
|
& $installer.FullName -Uninstall -System-Level -Verbose-Logging -Force-Uninstall
|
||||||
|
};
|
||||||
-
|
-
|
||||||
category: Disable built-in Windows features
|
category: Disable built-in Windows features
|
||||||
children:
|
children:
|
||||||
@@ -4522,5 +4532,9 @@ functions:
|
|||||||
parameters:
|
parameters:
|
||||||
- name: code
|
- name: code
|
||||||
- name: revertCode
|
- name: revertCode
|
||||||
|
optional: true
|
||||||
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code }}"
|
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code }}"
|
||||||
revertCode: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $revertCode }}"
|
revertCode: |-
|
||||||
|
{{ with $revertCode }}
|
||||||
|
PowerShell -ExecutionPolicy Unrestricted -Command "{{ . }}"
|
||||||
|
{{ end }}
|
||||||
|
|||||||
@@ -39,23 +39,37 @@ function validateCode(code: string, syntax: ILanguageSyntax): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoEmptyLines(code: string): void {
|
function ensureNoEmptyLines(code: string): void {
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
const lines = code.split(/\r\n|\r|\n/);
|
||||||
throw Error(`script has empty lines`);
|
if (lines.some((line) => line.trim().length === 0)) {
|
||||||
|
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
||||||
const lines = code.split('\n')
|
const allLines = code.split(/\r\n|\r|\n/);
|
||||||
.filter((line) => !shouldIgnoreLine(line, syntax));
|
const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||||
if (lines.length === 0) {
|
if (checkedLines.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
if (duplicateLines.length !== 0) {
|
if (duplicateLines.length !== 0) {
|
||||||
throw Error(`Duplicates detected in script:\n${duplicateLines.map((line, index) => `(${index}) - ${line}`).join('\n')}`);
|
throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printDuplicatedLines(allLines: string[]) {
|
||||||
|
return allLines
|
||||||
|
.map((line, index) => {
|
||||||
|
const occurrenceIndices = allLines
|
||||||
|
.map((e, i) => e === line ? i : '')
|
||||||
|
.filter(String);
|
||||||
|
const isDuplicate = occurrenceIndices.length > 1;
|
||||||
|
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
||||||
|
return `${indicator}[${index}] ${line}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
codeLine = codeLine.toLowerCase();
|
codeLine = codeLine.toLowerCase();
|
||||||
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
|
||||||
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
|
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
|
||||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub';
|
import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner';
|
||||||
|
|
||||||
describe('ParameterSubstitutionParser', () => {
|
describe('ParameterSubstitutionParser', () => {
|
||||||
describe('finds at expected positions', () => {
|
const sut = new ParameterSubstitutionParser();
|
||||||
// arrange
|
const runner = new SyntaxParserTestsRunner(sut);
|
||||||
const testCases = [
|
describe('finds as expected', () => {
|
||||||
|
runner.expectPosition(
|
||||||
{
|
{
|
||||||
name: 'matches single parameter',
|
name: 'single parameter',
|
||||||
code: '{{ $parameter }}!',
|
code: '{{ $parameter }}!',
|
||||||
expected: [new ExpressionPosition(0, 16)],
|
expected: [ new ExpressionPosition(0, 16) ],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'matches different parameters',
|
name: 'different parameters',
|
||||||
code: 'He{{ $firstParameter }} {{ $secondParameter }}!!',
|
code: 'He{{ $firstParameter }} {{ $secondParameter }}!!',
|
||||||
expected: [new ExpressionPosition(2, 23), new ExpressionPosition(24, 46)],
|
expected: [ new ExpressionPosition(2, 23), new ExpressionPosition(24, 46) ],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tolerates spaces around brackets',
|
name: 'tolerates lack of spaces around brackets',
|
||||||
code: 'He{{$firstParameter}}!!',
|
code: 'He{{$firstParameter}}!!',
|
||||||
expected: [new ExpressionPosition(2, 21) ],
|
expected: [new ExpressionPosition(2, 21) ],
|
||||||
},
|
},
|
||||||
@@ -28,44 +28,33 @@ describe('ParameterSubstitutionParser', () => {
|
|||||||
code: 'He{{ $ firstParameter }}!!',
|
code: 'He{{ $ firstParameter }}!!',
|
||||||
expected: [ ],
|
expected: [ ],
|
||||||
},
|
},
|
||||||
];
|
);
|
||||||
for (const testCase of testCases) {
|
|
||||||
it(testCase.name, () => {
|
|
||||||
const sut = new ParameterSubstitutionParser();
|
|
||||||
// act
|
|
||||||
const expressions = sut.findExpressions(testCase.code);
|
|
||||||
// assert
|
|
||||||
const actual = expressions.map((e) => e.position);
|
|
||||||
expect(actual).to.deep.equal(testCase.expected);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
describe('evaluates as expected', () => {
|
describe('evaluates as expected', () => {
|
||||||
const testCases = [ {
|
runner.expectResults(
|
||||||
name: 'single parameter',
|
{
|
||||||
code: '{{ $parameter }}',
|
name: 'single parameter',
|
||||||
args: new FunctionCallArgumentCollectionStub()
|
code: '{{ $parameter }}',
|
||||||
.withArgument('parameter', 'Hello world'),
|
args: (args) => args
|
||||||
expected: [ 'Hello world' ],
|
.withArgument('parameter', 'Hello world'),
|
||||||
},
|
expected: [ 'Hello world' ],
|
||||||
{
|
},
|
||||||
name: 'different parameters',
|
{
|
||||||
code: '{{ $firstParameter }} {{ $secondParameter }}!',
|
name: 'different parameters',
|
||||||
args: new FunctionCallArgumentCollectionStub()
|
code: '{{ $firstParameter }} {{ $secondParameter }}!',
|
||||||
.withArgument('firstParameter', 'Hello')
|
args: (args) => args
|
||||||
.withArgument('secondParameter', 'World'),
|
.withArgument('firstParameter', 'Hello')
|
||||||
expected: [ 'Hello', 'World' ],
|
.withArgument('secondParameter', 'World'),
|
||||||
}];
|
expected: [ 'Hello', 'World' ],
|
||||||
for (const testCase of testCases) {
|
},
|
||||||
it(testCase.name, () => {
|
{
|
||||||
const sut = new ParameterSubstitutionParser();
|
name: 'same parameters used twice',
|
||||||
// act
|
code: '{{ $letterH }}e{{ $letterL }}{{ $letterL }}o Wor{{ $letterL }}d!',
|
||||||
const expressions = sut.findExpressions(testCase.code);
|
args: (args) => args
|
||||||
// assert
|
.withArgument('letterL', 'l')
|
||||||
const actual = expressions.map((e) => e.evaluate(testCase.args));
|
.withArgument('letterH', 'H'),
|
||||||
expect(actual).to.deep.equal(testCase.expected);
|
expected: [ 'H', 'l', 'l', 'l' ],
|
||||||
});
|
},
|
||||||
}
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||||
|
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
|
import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub';
|
||||||
|
|
||||||
|
export class SyntaxParserTestsRunner {
|
||||||
|
constructor(private readonly sut: IExpressionParser) {
|
||||||
|
}
|
||||||
|
public expectPosition(...testCases: IExpectPositionTestCase[]) {
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
it(testCase.name, () => {
|
||||||
|
// act
|
||||||
|
const expressions = this.sut.findExpressions(testCase.code);
|
||||||
|
// assert
|
||||||
|
const actual = expressions.map((e) => e.position);
|
||||||
|
expect(actual).to.deep.equal(testCase.expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public expectResults(...testCases: IExpectResultTestCase[]) {
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
it(testCase.name, () => {
|
||||||
|
// arrange
|
||||||
|
const args = testCase.args(new FunctionCallArgumentCollectionStub());
|
||||||
|
// act
|
||||||
|
const expressions = this.sut.findExpressions(testCase.code);
|
||||||
|
// assert
|
||||||
|
const actual = expressions.map((e) => e.evaluate(args));
|
||||||
|
expect(actual).to.deep.equal(testCase.expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IExpectResultTestCase {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
args: (builder: FunctionCallArgumentCollectionStub) => FunctionCallArgumentCollectionStub;
|
||||||
|
expected: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IExpectPositionTestCase {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
expected: readonly ExpressionPosition[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||||
|
import { WithParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser';
|
||||||
|
import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner';
|
||||||
|
|
||||||
|
describe('WithParser', () => {
|
||||||
|
const sut = new WithParser();
|
||||||
|
const runner = new SyntaxParserTestsRunner(sut);
|
||||||
|
describe('finds as expected', () => {
|
||||||
|
runner.expectPosition(
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'when no scope is not used',
|
||||||
|
code: 'hello {{ with $parameter }}no usage{{ end }} here',
|
||||||
|
expected: [ new ExpressionPosition(6, 44) ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'when scope is used',
|
||||||
|
code: 'used here ({{ with $parameter }}value: {{ . }}{{ end }})',
|
||||||
|
expected: [ new ExpressionPosition(11, 55) ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'when used twice',
|
||||||
|
code: 'first: {{ with $parameter }}value: {{ . }}{{ end }}, second: {{ with $parameter }}no usage{{ end }}',
|
||||||
|
expected: [ new ExpressionPosition(7, 51), new ExpressionPosition(61, 99) ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tolerates lack of spaces around brackets',
|
||||||
|
code: 'no whitespaces {{with $parameter}}value: {{.}}{{end}}',
|
||||||
|
expected: [ new ExpressionPosition(15, 53) ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not tolerate space after dollar sign',
|
||||||
|
code: 'used here ({{ with $ parameter }}value: {{ . }}{{ end }})',
|
||||||
|
expected: [ ],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('ignores when syntax is unexpected', () => {
|
||||||
|
runner.expectPosition(
|
||||||
|
{
|
||||||
|
name: 'does not tolerate whitespace after with',
|
||||||
|
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
|
||||||
|
expected: [ ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not tolerate whitespace before dollar',
|
||||||
|
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
|
||||||
|
expected: [ ],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('ignores trailing and leading whitespaces and newlines inside scope', () => {
|
||||||
|
runner.expectResults(
|
||||||
|
{
|
||||||
|
name: 'does not render trailing whitespace after value',
|
||||||
|
code: '{{ with $parameter }}{{ . }}! {{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render trailing newline after value',
|
||||||
|
code: '{{ with $parameter }}{{ . }}!\r\n{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render leading newline before value',
|
||||||
|
code: '{{ with $parameter }}\r\n{{ . }}!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render leading whitespace before value',
|
||||||
|
code: '{{ with $parameter }} {{ . }}!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('does not render scope if argument is undefined', () => {
|
||||||
|
runner.expectResults(
|
||||||
|
{
|
||||||
|
name: 'does not render when value is undefined',
|
||||||
|
code: '{{ with $parameter }}dark{{ end }} ',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', undefined),
|
||||||
|
expected: [ '' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render when value is empty',
|
||||||
|
code: '{{ with $parameter }}dark {{.}}{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', ''),
|
||||||
|
expected: [ '' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render when argument is not provided',
|
||||||
|
code: '{{ with $parameter }}dark{{ end }}',
|
||||||
|
args: (args) => args,
|
||||||
|
expected: [ '' ],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('renders scope as expected', () => {
|
||||||
|
runner.expectResults(
|
||||||
|
{
|
||||||
|
name: 'renders scope even if value is not used',
|
||||||
|
code: '{{ with $parameter }}Hello world!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'renders value when it has value',
|
||||||
|
code: '{{ with $parameter }}{{ . }} world!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'renders value when whitespaces around brackets are missing',
|
||||||
|
code: '{{ with $parameter }}{{.}} world!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'renders value multiple times when it\'s used multiple times',
|
||||||
|
code: '{{ with $letterL }}He{{ . }}{{ . }}o wor{{ . }}d!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('letterL', 'l'),
|
||||||
|
expected: [ 'Hello world!' ],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -52,13 +52,13 @@ describe('ScriptCode', () => {
|
|||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
testName: 'cannot construct with duplicate lines',
|
testName: 'cannot construct with duplicate lines',
|
||||||
code: 'duplicate\nduplicate\ntest\nduplicate',
|
code: 'duplicate\nduplicate\nunique\nduplicate',
|
||||||
expectedMessage: 'Duplicates detected in script:\n(0) - duplicate\n(1) - duplicate',
|
expectedMessage: 'Duplicates detected in script:\n❌ (0,1,3)\t[0] duplicate\n❌ (0,1,3)\t[1] duplicate\n✅ [2] unique\n❌ (0,1,3)\t[3] duplicate',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'cannot construct with empty lines',
|
testName: 'cannot construct with empty lines',
|
||||||
code: 'line1\n\n\nline2',
|
code: 'line1\n\n\nline2',
|
||||||
expectedMessage: 'script has empty lines',
|
expectedMessage: 'Script has empty lines:\n\n (0) line1\n (1) ❌\n (2) ❌\n (3) line2',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// act
|
// act
|
||||||
|
|||||||
Reference in New Issue
Block a user