+ >
{{#if this.showSuggestions}}
{{#each this.suggestionItems as |item|}}
+ {{!-- template-lint-disable no-invalid-interactive --}}
-
+ {{!-- template-lint-disable no-triple-curlies --}}
{{{item.html}}}
{{/each}}
diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts
index 664d0899e..b9b9dc1f8 100644
--- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts
+++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts
@@ -25,6 +25,8 @@ export default class LabelContent extends Component {
// Private property
shouldShowExample = false;
+ circleMarker = '\u25CB';
+ infoMark = '\u24D8';
@computed('inputBlockUI')
get displayTextOverride(): string | undefined {
@@ -49,7 +51,6 @@ export default class LabelContent extends Component {
return undefined;
}
if (!this.tagDefs) {
- console.warn(`[metadata] tagDefs not provided for block with ui.item.tags: ${this.schemaBlock.displayText}`);
return undefined;
}
return resolveTags(tags, this.tagDefs, text => this.getLocalizedText(text));
diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs
index c45fe8217..5490b81cd 100644
--- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs
+++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs
@@ -1,8 +1,8 @@
{{assert 'Registries::SchemaBlockRenderer::Label::LabelContent requires a @schemaBlock' @schemaBlock}}
- {{~#if (eq this.itemMarker "circle")~}}◯ {{~/if~}}
+ local-class='DisplayText {{if this.displayTextOverride 'SubLabel'}}'>
+ {{~#if (eq this.itemMarker 'circle')~}}{{this.circleMarker}} {{~/if~}}
{{~this.localizedDisplayText~}}
{{~#if (and @isRequired (not @readonly))~}}
*
@@ -20,7 +20,7 @@
{{~/each~}}
{{~#if this.itemInfo~}}
- ⓘ
+ {{this.infoMark}}
{{this.itemInfo}}
diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/styles.scss
index a390a48f2..3057f4af8 100644
--- a/lib/osf-components/addon/components/registries/schema-block-renderer/styles.scss
+++ b/lib/osf-components/addon/components/registries/schema-block-renderer/styles.scss
@@ -9,4 +9,4 @@
.Element {
margin: 10px 0 0;
-}
\ No newline at end of file
+}
diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs
index 77c2c12bc..838c5bc47 100644
--- a/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs
+++ b/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs
@@ -2,11 +2,11 @@
{{#let (get renderers @schemaBlock.blockType) as |Renderer|}}
{{#if this.isLabelBlock}}
-
+
{{else}}
-
-
-
+
+
+
{{/if}}
{{/let}}
diff --git a/lib/osf-components/addon/components/registries/ui-visual-items/component.ts b/lib/osf-components/addon/components/registries/ui-visual-items/component.ts
index 9706587e0..a0af2b784 100644
--- a/lib/osf-components/addon/components/registries/ui-visual-items/component.ts
+++ b/lib/osf-components/addon/components/registries/ui-visual-items/component.ts
@@ -16,6 +16,8 @@ export default class UiVisualItems extends Component {
tagDefs?: TagDefs;
changeset?: any;
isTopLevel: boolean = false;
+ circleMarker = '\u25CB';
+ infoMark = '\u24D8';
@action
preventLabelFocus(event: Event) {
diff --git a/lib/osf-components/addon/components/registries/ui-visual-items/template.hbs b/lib/osf-components/addon/components/registries/ui-visual-items/template.hbs
index df6a088f2..3a8472b65 100644
--- a/lib/osf-components/addon/components/registries/ui-visual-items/template.hbs
+++ b/lib/osf-components/addon/components/registries/ui-visual-items/template.hbs
@@ -3,7 +3,7 @@
{{#if item.uiGroup.localizedTitle}}
-
{{#if (eq item.uiGroup.marker "circle")}}◯ {{/if}}{{item.uiGroup.localizedTitle}}
+
{{#if (eq item.uiGroup.marker 'circle')}}{{this.circleMarker}} {{/if}}{{item.uiGroup.localizedTitle}}
{{#each item.uiGroup.resolvedTags as |tag|}}
{{tag.localizedText}}
@@ -16,7 +16,7 @@
{{/each}}
{{#if item.uiGroup.localizedInfo}}
- ⓘ
+ {{this.infoMark}}
{{item.uiGroup.localizedInfo}}
diff --git a/lib/osf-components/addon/components/registries/value-check-mark/component.ts b/lib/osf-components/addon/components/registries/value-check-mark/component.ts
index b96ba22da..4fa766112 100644
--- a/lib/osf-components/addon/components/registries/value-check-mark/component.ts
+++ b/lib/osf-components/addon/components/registries/value-check-mark/component.ts
@@ -15,9 +15,9 @@ export default class ValueCheckMark extends Component {
return false;
}
if (Array.isArray(value)) {
- return value.some((row: any) =>
- Object.values(row).some((v: any) => v !== null && v !== undefined && v !== ''),
- );
+ return value.some((row: any) => Object.values(row).some(
+ (v: any) => v !== null && v !== undefined && v !== '',
+ ));
}
return true;
});
diff --git a/lib/registries/addon/drafts/draft/-components/register/component.ts b/lib/registries/addon/drafts/draft/-components/register/component.ts
index 803d8bf73..570c2f55e 100644
--- a/lib/registries/addon/drafts/draft/-components/register/component.ts
+++ b/lib/registries/addon/drafts/draft/-components/register/component.ts
@@ -85,7 +85,7 @@ export default class Register extends Component.extend({
@computed('draftRegistration.registrationResponses')
get wekoItemId(): string | null {
- return getWekoItemId(this.draftRegistration?.registrationResponses);
+ return getWekoItemId(this.draftRegistration && this.draftRegistration.registrationResponses);
}
@computed('draftRegistration.registrationSchema.name')
From a7a434a24b4c01733f122eaa7e797da98bbf0b3c Mon Sep 17 00:00:00 2001
From: yacchin1205 <968739+yacchin1205@users.noreply.github.com>
Date: Tue, 31 Mar 2026 21:47:20 +0900
Subject: [PATCH 7/7] Add workflow unit tests and fix acceptance test stub
---
tests/acceptance/guid-node/workflow-test.ts | 4 +
.../workflow/expression-evaluator-test.ts | 114 ++++++++
.../workflow/template-evaluator-test.ts | 258 ++++++++++++++++++
3 files changed, 376 insertions(+)
create mode 100644 tests/unit/guid-node/workflow/expression-evaluator-test.ts
create mode 100644 tests/unit/guid-node/workflow/template-evaluator-test.ts
diff --git a/tests/acceptance/guid-node/workflow-test.ts b/tests/acceptance/guid-node/workflow-test.ts
index dff3af131..b560280b2 100644
--- a/tests/acceptance/guid-node/workflow-test.ts
+++ b/tests/acceptance/guid-node/workflow-test.ts
@@ -53,6 +53,10 @@ function stubWorkflowRequests(
return Promise.resolve({ data: config.tasks || [] });
}
+ if (url.includes('/workflow/templates/') && method === 'GET') {
+ return Promise.resolve({ data: [] });
+ }
+
if (url.includes('/workflow/templates/') && method === 'POST') {
return Promise.resolve({
data: {
diff --git a/tests/unit/guid-node/workflow/expression-evaluator-test.ts b/tests/unit/guid-node/workflow/expression-evaluator-test.ts
new file mode 100644
index 000000000..1530f542a
--- /dev/null
+++ b/tests/unit/guid-node/workflow/expression-evaluator-test.ts
@@ -0,0 +1,114 @@
+import { evaluateExpression } from 'ember-osf-web/guid-node/workflow/-components/wizard-form/expression-evaluator';
+import { module, test } from 'qunit';
+
+module('Unit | Workflow | expression-evaluator', () => {
+ test('truthy field returns true', assert => {
+ assert.ok(evaluateExpression('name', { name: 'Alice' }));
+ });
+
+ test('missing field returns false', assert => {
+ assert.notOk(evaluateExpression('name', {}));
+ });
+
+ test('null field returns false', assert => {
+ assert.notOk(evaluateExpression('name', { name: null }));
+ });
+
+ test('empty string field returns false', assert => {
+ assert.notOk(evaluateExpression('name', { name: '' }));
+ });
+
+ test('true literal', assert => {
+ assert.ok(evaluateExpression('true', {}));
+ });
+
+ test('false literal', assert => {
+ assert.notOk(evaluateExpression('false', {}));
+ });
+
+ test('field prefixed with true is not confused', assert => {
+ assert.ok(evaluateExpression('trueness', { trueness: 'yes' }));
+ });
+
+ test('string equality', assert => {
+ assert.ok(evaluateExpression("status == 'active'", { status: 'active' }));
+ });
+
+ test('string inequality', assert => {
+ assert.ok(evaluateExpression("status != 'active'", { status: 'inactive' }));
+ });
+
+ test('OR left true', assert => {
+ assert.ok(evaluateExpression('a || b', { a: 'x', b: '' }));
+ });
+
+ test('OR right true', assert => {
+ assert.ok(evaluateExpression('a || b', { a: '', b: 'x' }));
+ });
+
+ test('OR both false', assert => {
+ assert.notOk(evaluateExpression('a || b', { a: '', b: '' }));
+ });
+
+ test('AND both true', assert => {
+ assert.ok(evaluateExpression('a && b', { a: 'x', b: 'y' }));
+ });
+
+ test('AND left false', assert => {
+ assert.notOk(evaluateExpression('a && b', { a: '', b: 'y' }));
+ });
+
+ test('NOT truthy', assert => {
+ assert.notOk(evaluateExpression('!a', { a: 'x' }));
+ });
+
+ test('NOT falsy', assert => {
+ assert.ok(evaluateExpression('!a', { a: '' }));
+ });
+
+ test('double NOT', assert => {
+ assert.ok(evaluateExpression('!!a', { a: 'x' }));
+ });
+
+ test('AND binds tighter than OR', assert => {
+ // false || (true && true) => true
+ assert.ok(evaluateExpression('a || b && c', { a: '', b: 'x', c: 'y' }));
+ });
+
+ test('NOT binds tighter than AND', assert => {
+ // (!false) && true => true
+ assert.ok(evaluateExpression('!a && b', { a: '', b: 'x' }));
+ });
+
+ test('parentheses override precedence', assert => {
+ // !(false && true) => true
+ assert.ok(evaluateExpression('!(a && b)', { a: '', b: 'x' }));
+ });
+
+ test('chained OR parses all operands', assert => {
+ assert.ok(evaluateExpression('a || b || c', { a: '', b: '', c: 'x' }));
+ });
+
+ test('chained OR all empty', assert => {
+ assert.notOk(evaluateExpression('a || b || c', { a: '', b: '', c: '' }));
+ });
+
+ test('display_template style: last || first || middle', assert => {
+ assert.ok(evaluateExpression(
+ 'last || first || middle',
+ { last: '', first: 'Taro', middle: '' },
+ ));
+ });
+
+ test('throws on empty expression', assert => {
+ assert.throws(() => evaluateExpression('', {}), /empty expression/);
+ });
+
+ test('throws on trailing garbage', assert => {
+ assert.throws(() => evaluateExpression('a b', { a: 'x' }), /unexpected/);
+ });
+
+ test('throws on unterminated string', assert => {
+ assert.throws(() => evaluateExpression("a == 'open", {}), /unterminated/);
+ });
+});
diff --git a/tests/unit/guid-node/workflow/template-evaluator-test.ts b/tests/unit/guid-node/workflow/template-evaluator-test.ts
new file mode 100644
index 000000000..a4c49ffa5
--- /dev/null
+++ b/tests/unit/guid-node/workflow/template-evaluator-test.ts
@@ -0,0 +1,258 @@
+import {
+ evaluateTemplate,
+ hasTemplateDirectives,
+} from 'ember-osf-web/guid-node/workflow/-components/wizard-form/template-evaluator';
+import { module, test } from 'qunit';
+
+module('Unit | Workflow | template-evaluator', () => {
+ // -- hasTemplateDirectives ------------------------------------------------
+
+ test('detects {{ }}', assert => {
+ assert.ok(hasTemplateDirectives('Hello {{ name }}'));
+ });
+
+ test('detects {% %}', assert => {
+ assert.ok(hasTemplateDirectives('{% if x %}yes{% endif %}'));
+ });
+
+ test('plain text has no directives', assert => {
+ assert.notOk(hasTemplateDirectives('no directives here'));
+ });
+
+ // -- variable interpolation -----------------------------------------------
+
+ test('interpolates variable', assert => {
+ assert.equal(evaluateTemplate('Hello {{ name }}!', { name: 'Alice' }), 'Hello Alice!');
+ });
+
+ test('missing variable renders empty', assert => {
+ assert.equal(evaluateTemplate('Hello {{ name }}!', {}), 'Hello !');
+ });
+
+ test('multiple variables', assert => {
+ assert.equal(
+ evaluateTemplate('{{ first }} {{ last }}', { first: 'A', last: 'B' }),
+ 'A B',
+ );
+ });
+
+ // -- if/else/endif --------------------------------------------------------
+
+ test('if truthy renders body', assert => {
+ assert.equal(
+ evaluateTemplate('{% if show %}visible{% endif %}', { show: 'yes' }),
+ 'visible',
+ );
+ });
+
+ test('if falsy skips body', assert => {
+ assert.equal(
+ evaluateTemplate('{% if show %}visible{% endif %}', { show: '' }),
+ '',
+ );
+ });
+
+ test('if/else renders else branch when falsy', assert => {
+ assert.equal(
+ evaluateTemplate('{% if show %}yes{% else %}no{% endif %}', { show: '' }),
+ 'no',
+ );
+ });
+
+ test('if/elif/else chain', assert => {
+ assert.equal(
+ evaluateTemplate(
+ '{% if a %}A{% elif b %}B{% else %}C{% endif %}',
+ { a: '', b: 'x' },
+ ),
+ 'B',
+ );
+ });
+
+ test('nested if', assert => {
+ assert.equal(
+ evaluateTemplate(
+ '{% if a %}{% if b %}AB{% endif %}{% endif %}',
+ { a: 'x', b: 'y' },
+ ),
+ 'AB',
+ );
+ });
+
+ test('nested if with outer false', assert => {
+ assert.equal(
+ evaluateTemplate(
+ '{% if a %}{% if b %}AB{% endif %}{% endif %}',
+ { a: '', b: 'y' },
+ ),
+ '',
+ );
+ });
+
+ // -- expressions in if ----------------------------------------------------
+
+ test('if with or expression', assert => {
+ assert.equal(
+ evaluateTemplate('{% if a or b %}yes{% endif %}', { a: '', b: 'x' }),
+ 'yes',
+ );
+ });
+
+ test('if with and expression', assert => {
+ assert.equal(
+ evaluateTemplate('{% if a and b %}yes{% endif %}', { a: 'x', b: '' }),
+ '',
+ );
+ });
+
+ test('if with not expression', assert => {
+ assert.equal(
+ evaluateTemplate('{% if not a %}empty{% endif %}', { a: '' }),
+ 'empty',
+ );
+ });
+
+ test('if with comparison', assert => {
+ assert.equal(
+ evaluateTemplate("{% if status == 'done' %}ok{% endif %}", { status: 'done' }),
+ 'ok',
+ );
+ });
+
+ // -- for loop -------------------------------------------------------------
+
+ test('for loop iterates', assert => {
+ assert.equal(
+ evaluateTemplate('{% for x in items %}[{{ x }}]{% endfor %}', { items: ['a', 'b'] }),
+ '[a][b]',
+ );
+ });
+
+ test('for loop with empty array', assert => {
+ assert.equal(
+ evaluateTemplate('{% for x in items %}[{{ x }}]{% endfor %}', { items: [] }),
+ '',
+ );
+ });
+
+ test('for loop with object access', assert => {
+ assert.equal(
+ evaluateTemplate(
+ '{% for p in people %}{{ p.name }} {% endfor %}',
+ { people: [{ name: 'A' }, { name: 'B' }] },
+ ),
+ 'A B ',
+ );
+ });
+
+ // -- filters --------------------------------------------------------------
+
+ test('default filter on missing value', assert => {
+ assert.equal(
+ evaluateTemplate("{{ name | default('N/A') }}", {}),
+ 'N/A',
+ );
+ });
+
+ test('default filter on present value', assert => {
+ assert.equal(
+ evaluateTemplate("{{ name | default('N/A') }}", { name: 'Alice' }),
+ 'Alice',
+ );
+ });
+
+ test('length filter', assert => {
+ assert.equal(
+ evaluateTemplate('{{ items | length }}', { items: [1, 2, 3] }),
+ '3',
+ );
+ });
+
+ // -- whitespace trimming --------------------------------------------------
+
+ test('trim before with {%-', assert => {
+ assert.equal(
+ evaluateTemplate('hello {%- if true %} world{% endif %}', {}),
+ 'hello world',
+ );
+ });
+
+ test('trim after with -%}', assert => {
+ assert.equal(
+ evaluateTemplate('{% if true -%} hello{% endif %}', {}),
+ 'hello',
+ );
+ });
+
+ // -- dot access and bracket access ----------------------------------------
+
+ test('dot access', assert => {
+ assert.equal(
+ evaluateTemplate('{{ user.name }}', { user: { name: 'Bob' } }),
+ 'Bob',
+ );
+ });
+
+ test('bracket access', assert => {
+ assert.equal(
+ evaluateTemplate("{{ data['key'] }}", { data: { key: 'val' } }),
+ 'val',
+ );
+ });
+
+ // -- display_template real-world pattern -----------------------------------
+
+ test('display_template name pattern with all fields', assert => {
+ const tpl = '{% if last or first %}{% if last %}{{ last }}, {% endif %}{{ first }} {{ middle }}{% endif %}';
+ assert.equal(
+ evaluateTemplate(tpl, { last: 'Smith', first: 'John', middle: 'A' }),
+ 'Smith, John A',
+ );
+ });
+
+ test('display_template name pattern with only first', assert => {
+ const tpl = '{% if last or first %}{% if last %}{{ last }}, {% endif %}{{ first }} {{ middle }}{% endif %}';
+ assert.equal(
+ evaluateTemplate(tpl, { last: '', first: 'Taro', middle: '' }),
+ 'Taro ',
+ );
+ });
+
+ test('display_template name pattern with all empty', assert => {
+ const tpl = '{% if last or first %}{% if last %}{{ last }}, {% endif %}{{ first }} {{ middle }}{% endif %}';
+ assert.equal(
+ evaluateTemplate(tpl, { last: '', first: '', middle: '' }),
+ '',
+ );
+ });
+
+ // -- error handling -------------------------------------------------------
+
+ test('throws on unclosed if', assert => {
+ assert.throws(
+ () => evaluateTemplate('{% if x %}hello', { x: 'y' }),
+ /unclosed/i,
+ );
+ });
+
+ test('throws on unclosed {{', assert => {
+ assert.throws(
+ () => evaluateTemplate('{{ name', {}),
+ /unclosed/i,
+ );
+ });
+
+ test('throws on unknown tag', assert => {
+ assert.throws(
+ () => evaluateTemplate('{% while true %}{% endwhile %}', {}),
+ /unknown tag/i,
+ );
+ });
+
+ test('throws on unknown filter', assert => {
+ assert.throws(
+ () => evaluateTemplate('{{ x | bogus }}', { x: 'v' }),
+ /unknown filter/i,
+ );
+ });
+});