mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
Ui/transform templates (#9981)
Add CRUD capabilities on transform templates. Disallow read or edit for built-ins.
This commit is contained in:
@@ -55,12 +55,11 @@ export default ApplicationAdapter.extend({
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.ajax(this.url(query.backend, type.modelName, query.id), 'GET').then(result => {
|
||||
// CBS TODO: Add name to response and unmap name <> id on models
|
||||
return {
|
||||
id: query.id,
|
||||
...result,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// buildUrl(modelName, id, snapshot, requestType, query, returns) {},
|
||||
});
|
||||
|
||||
32
ui/app/components/transform-list-item.js
Normal file
32
ui/app/components/transform-list-item.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @module TransformListItem
|
||||
* TransformListItem components are used for the list items for the Transform Secret Engines for all but Transformations.
|
||||
* This component automatically handles read-only list items if capabilities are not granted or the item is internal only.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <TransformListItem @item={item} @itemPath="role/my-item" @itemType="role" />
|
||||
* ```
|
||||
* @param {object} item - item refers to the model item used on the list item partial
|
||||
* @param {string} itemPath - usually the id of the item, but can be prefixed with the model type (see transform/role)
|
||||
* @param {string} [itemType] - itemType is used to calculate whether an item is readable or
|
||||
*/
|
||||
|
||||
import { computed } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
item: null,
|
||||
itemPath: '',
|
||||
itemType: '',
|
||||
|
||||
itemViewable: computed('item', 'itemType', function() {
|
||||
const item = this.get('item');
|
||||
if (this.itemType === 'alphabet' || this.itemType === 'template') {
|
||||
return !item.get('id').startsWith('builtin/');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
backendType: 'transform',
|
||||
});
|
||||
3
ui/app/components/transform-template-edit.js
Normal file
3
ui/app/components/transform-template-edit.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import TransformBase from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({});
|
||||
@@ -80,15 +80,14 @@ const SECRET_BACKENDS = {
|
||||
editComponent: 'transform-role-edit',
|
||||
},
|
||||
{
|
||||
name: 'templates',
|
||||
name: 'template',
|
||||
modelPrefix: 'template/',
|
||||
label: 'Templates',
|
||||
searchPlaceholder: 'Filter templates',
|
||||
item: 'templates',
|
||||
item: 'template',
|
||||
create: 'Create template',
|
||||
tab: 'templates',
|
||||
tab: 'template',
|
||||
editComponent: 'transform-template-edit',
|
||||
hideCreate: true,
|
||||
},
|
||||
{
|
||||
name: 'alphabets',
|
||||
|
||||
@@ -1,7 +1,52 @@
|
||||
import { computed } from '@ember/object';
|
||||
import DS from 'ember-data';
|
||||
import { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import attachCapabilities from 'vault/lib/attach-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
export default DS.Model.extend({
|
||||
name: DS.attr('string'),
|
||||
alphabet: DS.belongsTo('transform/alphabet'),
|
||||
transformations: DS.hasMany('transformation'),
|
||||
const { attr } = DS;
|
||||
|
||||
const Model = DS.Model.extend({
|
||||
idPrefix: 'template/',
|
||||
idForNav: computed('id', 'idPrefix', function() {
|
||||
let modelId = this.id || '';
|
||||
return `${this.idPrefix}${modelId}`;
|
||||
}),
|
||||
|
||||
name: attr('string', {
|
||||
label: 'Name',
|
||||
fieldValue: 'id',
|
||||
readOnly: true,
|
||||
subText:
|
||||
'Templates allow Vault to determine what and how to capture the value to be transformed. This cannot be edited later.',
|
||||
}),
|
||||
type: attr('string', { defaultValue: 'regex' }),
|
||||
pattern: attr('string', {
|
||||
subText: 'The template’s pattern defines the data format. Expressed in regex.',
|
||||
}),
|
||||
alphabet: attr('array', {
|
||||
subText:
|
||||
'Alphabet defines a set of characters (UTF-8) that is used for FPE to determine the validity of plaintext and ciphertext values. You can choose a built-in one, or create your own.',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
label: 'Alphabet',
|
||||
models: ['transform/alphabet'],
|
||||
selectLimit: 1,
|
||||
}),
|
||||
|
||||
attrs: computed('pattern', 'alphabet', function() {
|
||||
let keys = ['name', 'pattern', 'alphabet'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}),
|
||||
|
||||
editableAttrs: computed('pattern', 'alphabet', function() {
|
||||
let keys = ['pattern', 'alphabet'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}),
|
||||
|
||||
backend: attr('string', { readOnly: true }),
|
||||
});
|
||||
|
||||
export default attachCapabilities(Model, {
|
||||
updatePath: apiPath`${'backend'}/template/${'id'}`,
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ export default Route.extend({
|
||||
case 'role':
|
||||
modelType = 'transform/role';
|
||||
break;
|
||||
case 'templates':
|
||||
case 'template':
|
||||
modelType = 'transform/template';
|
||||
break;
|
||||
case 'alphabets':
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
extractLazyPaginatedData(payload) {
|
||||
let ret;
|
||||
|
||||
34
ui/app/serializers/transform/template.js
Normal file
34
ui/app/serializers/transform/template.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
payload.data.name = payload.id;
|
||||
if (payload.data.alphabet) {
|
||||
payload.data.alphabet = [payload.data.alphabet];
|
||||
}
|
||||
return this._super(store, primaryModelClass, payload, id, requestType);
|
||||
},
|
||||
|
||||
serialize() {
|
||||
let json = this._super(...arguments);
|
||||
if (json.alphabet && Array.isArray(json.alphabet)) {
|
||||
// Templates should only ever have one alphabet
|
||||
json.alphabet = json.alphabet[0];
|
||||
}
|
||||
return json;
|
||||
},
|
||||
|
||||
extractLazyPaginatedData(payload) {
|
||||
let ret;
|
||||
ret = payload.data.keys.map(key => {
|
||||
let model = {
|
||||
id: key,
|
||||
};
|
||||
if (payload.backend) {
|
||||
model.backend = payload.backend;
|
||||
}
|
||||
return model;
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
66
ui/app/templates/components/transform-list-item.hbs
Normal file
66
ui/app/templates/components/transform-list-item.hbs
Normal file
@@ -0,0 +1,66 @@
|
||||
{{#if (and itemViewable item.updatePath.canRead)}}
|
||||
{{#linked-block
|
||||
"vault.cluster.secrets.backend.show"
|
||||
itemPath
|
||||
class="list-item-row"
|
||||
data-test-secret-link=itemPath
|
||||
encode=true
|
||||
queryParams=(secret-query-params backendType)
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{item.id}}
|
||||
@queryParams={{query-params type=modelType}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
<Icon
|
||||
@glyph='file-outline'
|
||||
@class="has-text-grey-light"/>
|
||||
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
|
||||
</SecretLink>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
{{#if (or item.updatePath.canRead item.updatePath.canUpdate)}}
|
||||
<PopupMenu name="secret-menu">
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{#if item.updatePath.canRead}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{itemPath}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
Details
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if item.updatePath.canUpdate}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="edit"
|
||||
@secret={{itemPath}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
Edit
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/linked-block}}
|
||||
{{else}}
|
||||
<div class="list-item-row" data-test-view-only-list-item>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-12 has-text-grey has-text-weight-semibold">
|
||||
<Icon
|
||||
@glyph='file-outline'
|
||||
@class="has-text-grey-light"/>
|
||||
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
123
ui/app/templates/components/transform-template-edit.hbs
Normal file
123
ui/app/templates/components/transform-template-edit.hbs
Normal file
@@ -0,0 +1,123 @@
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
{{key-value-header
|
||||
baseKey=(hash display=model.id id=model.idForNav)
|
||||
path="vault.cluster.secrets.backend.list"
|
||||
mode=mode
|
||||
root=root
|
||||
showCurrent=true
|
||||
}}
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-secret-header="true">
|
||||
{{#if (eq mode "create") }}
|
||||
Create Template
|
||||
{{else if (eq mode "edit")}}
|
||||
Edit Template
|
||||
{{else}}
|
||||
Template <code>{{model.id}}</code>
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (eq mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if capabilities.canDelete}}
|
||||
<button
|
||||
class="toolbar-link"
|
||||
onclick={{action "delete"}}
|
||||
data-test-transformation-template-delete
|
||||
>
|
||||
Delete template
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if capabilities.canUpdate }}
|
||||
<ToolbarSecretLink
|
||||
@secret={{concat model.idPrefix model.id}}
|
||||
@mode="edit"
|
||||
@data-test-edit-link=true
|
||||
@replace=true
|
||||
>
|
||||
Edit template
|
||||
</ToolbarSecretLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or (eq mode 'edit') (eq mode 'create'))}}
|
||||
<form onsubmit={{action "createOrUpdate" mode}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
{{message-error model=model}}
|
||||
<NamespaceReminder @mode={{mode}} @noun="Transform role" />
|
||||
{{#each model.attrs as |attr|}}
|
||||
{{#if (and (eq attr.name 'name') (eq mode 'edit')) }}
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
{{#if attr.options.subText}}
|
||||
<p class="sub-text">{{attr.options.subText}}</p>
|
||||
{{/if}}
|
||||
<input
|
||||
data-test-input={{attr.name}}
|
||||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
value={{or (get model attr.name) attr.options.defaultValue}}
|
||||
readonly
|
||||
class="field input is-readOnly"
|
||||
type={{attr.type}}
|
||||
/>
|
||||
{{else}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={{buttonDisabled}}
|
||||
class="button is-primary"
|
||||
data-test-role-transform-create=true
|
||||
>
|
||||
{{#if (eq mode 'create')}}
|
||||
Create role
|
||||
{{else if (eq mode 'edit')}}
|
||||
Save
|
||||
{{/if}}
|
||||
</button>
|
||||
{{#secret-link
|
||||
mode=(if (eq mode "create") "list" "show")
|
||||
class="button"
|
||||
secret=(concat model.idPrefix model.id)
|
||||
}}
|
||||
Cancel
|
||||
{{/secret-link}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each model.attrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
{{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
|
||||
{{else if (eq attr.type "array")}}
|
||||
{{info-table-row
|
||||
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
|
||||
value=(get model attr.name)
|
||||
type=attr.type
|
||||
viewAll=attr.name
|
||||
}}
|
||||
{{else}}
|
||||
{{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -1,77 +1,5 @@
|
||||
{{!-- TODO do not let click if !canRead --}}
|
||||
{{#if (eq options.item "role")}}
|
||||
{{#let (concat options.modelPrefix item.id) as |itemPath|}}
|
||||
{{#linked-block
|
||||
"vault.cluster.secrets.backend.show"
|
||||
itemPath
|
||||
class="list-item-row"
|
||||
data-test-secret-link=itemPath
|
||||
encode=true
|
||||
queryParams=(secret-query-params backendModel.type)
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{item.id}}
|
||||
@queryParams={{if (eq backendModel.type "transform") (query-params tab="actions") ""}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
<Icon
|
||||
@glyph='file-outline'
|
||||
@class="has-text-grey-light"/>
|
||||
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
|
||||
</SecretLink>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
{{#if (or item.updatePath.canRead item.updatePath.canUpdate)}}
|
||||
<PopupMenu name="secret-menu">
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{#if (or item.versionPath.isLoading item.secretPath.isLoading)}}
|
||||
<li class="action">
|
||||
<button disabled type="button" class="link button is-loading is-transparent">
|
||||
loading
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if item.updatePath.canRead}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{itemPath}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
Details
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if item.updatePath.canUpdate}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="edit"
|
||||
@secret={{itemPath}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
Edit
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/linked-block}}
|
||||
{{/let}}
|
||||
{{else}}
|
||||
<div class="list-item-row">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-12 has-text-grey has-text-weight-semibold">
|
||||
<Icon
|
||||
@glyph='file-outline'
|
||||
@class="has-text-grey-light"/>
|
||||
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<TransformListItem
|
||||
@item={{item}}
|
||||
@itemPath={{concat options.modelPrefix item.id}}
|
||||
@itemType={{options.item}}
|
||||
/>
|
||||
|
||||
121
ui/tests/integration/components/transform-list-item-test.js
Normal file
121
ui/tests/integration/components/transform-list-item-test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import EmberObject from '@ember/object';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, findAll, click } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | transform-list-item', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders un-clickable item if no read capability', async function(assert) {
|
||||
let item = EmberObject.create({
|
||||
id: 'foo',
|
||||
updatePath: {
|
||||
canRead: false,
|
||||
canDelete: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
});
|
||||
this.set('itemPath', 'role/foo');
|
||||
this.set('itemType', 'role');
|
||||
this.set('item', item);
|
||||
await render(hbs`<TransformListItem
|
||||
@item={{item}}
|
||||
@itemPath={{itemPath}}
|
||||
@itemType={{itemType}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-view-only-list-item]').exists('shows view only list item');
|
||||
assert.dom('[data-test-view-only-list-item]').hasText(item.id, 'has correct label');
|
||||
});
|
||||
|
||||
test('it is clickable with details menu item if read capability', async function(assert) {
|
||||
let item = EmberObject.create({
|
||||
id: 'foo',
|
||||
updatePath: {
|
||||
canRead: true,
|
||||
canDelete: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
});
|
||||
this.set('itemPath', 'template/foo');
|
||||
this.set('itemType', 'template');
|
||||
this.set('item', item);
|
||||
await render(hbs`<TransformListItem
|
||||
@item={{item}}
|
||||
@itemPath={{itemPath}}
|
||||
@itemType={{itemType}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-secret-link="template/foo"]').exists('shows clickable list item');
|
||||
await click('button.popup-menu-trigger');
|
||||
assert.equal(findAll('.popup-menu-content li').length, 1, 'has one option');
|
||||
});
|
||||
|
||||
test('it has details and edit menu item if read & edit capabilities', async function(assert) {
|
||||
let item = EmberObject.create({
|
||||
id: 'foo',
|
||||
updatePath: {
|
||||
canRead: true,
|
||||
canDelete: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
});
|
||||
this.set('itemPath', 'alphabet/foo');
|
||||
this.set('itemType', 'alphabet');
|
||||
this.set('item', item);
|
||||
await render(hbs`<TransformListItem
|
||||
@item={{item}}
|
||||
@itemPath={{itemPath}}
|
||||
@itemType={{itemType}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-secret-link="alphabet/foo"]').exists('shows clickable list item');
|
||||
await click('button.popup-menu-trigger');
|
||||
assert.equal(findAll('.popup-menu-content li').length, 2, 'has both options');
|
||||
});
|
||||
|
||||
test('it is not clickable if built-in template with all capabilities', async function(assert) {
|
||||
let item = EmberObject.create({
|
||||
id: 'builtin/foo',
|
||||
updatePath: {
|
||||
canRead: true,
|
||||
canDelete: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
});
|
||||
this.set('itemPath', 'template/builtin/foo');
|
||||
this.set('itemType', 'template');
|
||||
this.set('item', item);
|
||||
await render(hbs`<TransformListItem
|
||||
@item={{item}}
|
||||
@itemPath={{itemPath}}
|
||||
@itemType={{itemType}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-view-only-list-item]').exists('shows view only list item');
|
||||
assert.dom('[data-test-view-only-list-item]').hasText(item.id, 'has correct label');
|
||||
});
|
||||
|
||||
test('it is not clickable if built-in alphabet', async function(assert) {
|
||||
let item = EmberObject.create({
|
||||
id: 'builtin/foo',
|
||||
updatePath: {
|
||||
canRead: true,
|
||||
canDelete: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
});
|
||||
this.set('itemPath', 'alphabet/builtin/foo');
|
||||
this.set('itemType', 'alphabet');
|
||||
this.set('item', item);
|
||||
await render(hbs`<TransformListItem
|
||||
@item={{item}}
|
||||
@itemPath={{itemPath}}
|
||||
@itemType={{itemType}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-view-only-list-item]').exists('shows view only list item');
|
||||
assert.dom('[data-test-view-only-list-item]').hasText(item.id, 'has correct label');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { module, skip } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | transform-template-edit', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
skip('it renders', async function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`{{transform-template-edit}}`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), '');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
{{#transform-template-edit}}
|
||||
template block text
|
||||
{{/transform-template-edit}}
|
||||
`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'template block text');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user