UI: Add example modal to policy form (#21583)

* created new JsonTemplate component

* used JsonTemplate in modal PolicyTemplate to replace code there

* renamed component and fixed when the editor content shows up

* changed PolicyForm to render example modal only conditionally. added desription to policy-example.js

* fixed bug in policy-example.js & edited description of that file, removed functionality from policy-template.js (it is already in policy-example.js)

* changed margin on text to better match Figma design, added example modal for when editing a policy

* added tests for PolicyExample in policy-example-tests

* added PolicyForm tests for (1) cancelling the creation/edit of policy and (2) properly rendering the policy example modal

* add changelog

* clean up code by removing unnecessary comments

* changed a conditional in policy-form.hbs for better readability (Kianna's comment)

* fixed description in policy-example.js, changed wording for RGP example, changed wording in policy-form-test.js

* added 2 more asserts in policy-form-test.js. Changed some naming for selectors in the test file

* added EGP policy to PolicyExample component, moved some functionality from .hbs to .js file and vise versa

* added tests to policy-exammple-test.js and policy-form-test.js to account for new EGP policy

* simplified all PolicyExample tests in policy-exmaple-test.js

* removed beforeEach hook in policy-exmaple-test.js
This commit is contained in:
malinac02
2023-07-20 09:59:52 -07:00
committed by GitHub
parent a71c174e79
commit a4f67a6b2b
13 changed files with 520 additions and 83 deletions

3
changelog/21583.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:feature
ui: add example modal to policy form
```

View File

@@ -27,37 +27,7 @@
</nav>
{{/if}}
{{#if this.showExamplePolicy}}
<div class="has-bottom-margin-s">
{{#if (eq this.policy.policyType "acl")}}
<p>
ACL Policies are written in Hashicorp Configuration Language (
<ExternalLink @href="https://github.com/hashicorp/hcl">HCL</ExternalLink>
) or JSON and describe which paths in Vault a user or machine is allowed to access. Here is an example policy:
</p>
{{else}}
<p class="has-bottom-margin-s">
Role Governing Policies (RGPs) are tied to client tokens or identities which is similar to
<DocLink @path="/vault/tutorials/policies/policies">ACL policies</DocLink>. They use
<DocLink @path="/vault/docs/enterprise/sentinel">Sentinel</DocLink>
as a language framework to enable fine-grained policy decisions.
</p>
<p>
Here is an example policy that uses RGP to restrict access to the
<code class="tag is-marginless is-paddingless">admin</code>
policy such that a user named James or has the
<code class="tag is-marginless is-paddingless">Team Lead</code>
role can manage the
<code class="tag is-marginless is-paddingless">admin</code>
policy:
</p>
{{/if}}
</div>
<JsonEditor
@value={{get this.policyTemplates this.policy.policyType}}
@mode="ruby"
@readOnly={{true}}
@showToolbar={{true}}
/>
<PolicyExample @policyType={{this.policy.policyType}} />
{{else}}
<Select
@name="policyType"

View File

@@ -37,39 +37,6 @@ export default class PolicyTemplate extends Component {
{ label: 'Role Governing Policy', value: 'rgp', isDisabled: !this.version.hasSentinel },
];
}
// formatting here is purposeful so that whitespace renders correctly in JsonEditor
policyTemplates = {
acl: `
# Grant 'create', 'read' , 'update', and list permission
# to paths prefixed by 'secret/*'
path "secret/*" {
capabilities = [ "create", "read", "update", "list" ]
}
# Even though we allowed secret/*, this line explicitly denies
# secret/super-secret. This takes precedence.
path "secret/super-secret" {
capabilities = ["deny"]
}
`,
rgp: `
# Import strings library that exposes common string operations
import "strings"
# Conditional rule (precond) checks the incoming request endpoint
# targeted to sys/policies/acl/admin
precond = rule {
strings.has_prefix(request.path, "sys/policies/admin")
}
# Vault checks to see if the request was made by an entity
# named James Thomas or Team Lead role defined as its metadata
main = rule when precond {
identity.entity.metadata.role is "Team Lead" or
identity.entity.name is "James Thomas"
}
`,
};
@action
setPolicyType(type) {

View File

@@ -42,7 +42,6 @@
{{else}}
<JsonEditor
@title="Policy"
@helpText="You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field"
@showToolbar={{false}}
@value={{@model.policy}}
@valueUpdated={{action (mut @model.policy)}}
@@ -55,7 +54,6 @@
{{! EDITING - no file upload toggle}}
<JsonEditor
@title="Policy"
@helpText="You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field"
@value={{@model.policy}}
@valueUpdated={{action (mut @model.policy)}}
@mode="ruby"
@@ -63,13 +61,26 @@
data-test-policy-editor
/>
{{/if}}
</div>
{{#each @model.additionalAttrs as |attr|}}
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
{{/each}}
</div>
<div class="has-bottom-margin-m">
<p>
<div class="has-top-margin-xs">
<span class="is-size-9 has-text-grey has-bottom-margin-l">
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field.
</span>
{{! Only renders button (and modal) if not already in the "create policy" modal }}
{{#if @renderPolicyExampleModal}}
<span class="is-size-9 has-text-grey has-bottom-margin-l">
See
<button
type="button"
class="text-button has-text-info"
{{on "click" (fn (mut this.showTemplateModal))}}
data-test-policy-example-button
>
example template
</button>.
</span>
{{! Only renders more information if already in the "create policy" modal }}
{{else}}
<p class="has-top-margin-l">
More information about
{{uppercase @model.policyType}}
policies can be found
@@ -83,6 +94,12 @@
here.
</DocLink>
</p>
{{/if}}
</div>
</div>
{{#each @model.additionalAttrs as |attr|}}
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
{{/each}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
@@ -106,3 +123,26 @@
</div>
</div>
</form>
{{! SAMPLE POLICY MODAL. Only renders modal if not already in create policy modal }}
{{#if @renderPolicyExampleModal}}
<Modal
@title="Example {{uppercase @model.policyType}} Policy"
@onClose={{fn (mut this.showTemplateModal) false}}
@isActive={{this.showTemplateModal}}
@showCloseButton={{true}}
data-test-policy-example-modal
>
<section class="modal-card-body">
{{! code-mirror modifier does not render value initially until focus event fires }}
{{! wait until the Modal is rendered and then show the PolicyExample (contains JsonEditor) }}
{{#if this.showTemplateModal}}
<PolicyExample @policyType={{@model.policyType}} />
{{/if}}
</section>
<div class="modal-card-head has-border-top-light">
<button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
Close
</button>
</div>
</Modal>
{{/if}}

View File

@@ -19,11 +19,13 @@ import { tracked } from '@glimmer/tracking';
* @model={{this.model}}
* @onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
* @onCancel={{transition-to "vault.cluster.policies.index"}}
* @renderPolicyExampleModal={{true}}
* />
* ```
* @callback onCancel - callback triggered when cancel button is clicked
* @callback onSave - callback triggered when save button is clicked. Passes saved model
* @param {object} model - ember data model from createRecord
* @param {boolean} renderPolicyExampleModal - whether or not the policy form should render the modal containing the policy example
*/
export default class PolicyFormComponent extends Component {
@@ -31,6 +33,7 @@ export default class PolicyFormComponent extends Component {
@tracked errorBanner = '';
@tracked showFileUpload = false;
@tracked showTemplateModal = false;
@task
*save(event) {

View File

@@ -21,4 +21,5 @@
@model={{this.model}}
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
@onCancel={{transition-to "vault.cluster.policies.index"}}
@renderPolicyExampleModal={{true}}
/>

View File

@@ -46,4 +46,5 @@
@model={{this.model}}
@onSave={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
@onCancel={{transition-to "vault.cluster.policy.show" this.model.policyType this.model.name}}
@renderPolicyExampleModal={{true}}
/>

View File

@@ -38,6 +38,7 @@
}}
class={{if @readOnly "readonly-codemirror"}}
data-test-component="code-mirror-modifier"
data-test-example-modal-json-text
></div>
{{#if @helpText}}

View File

@@ -0,0 +1,50 @@
<div class="has-bottom-margin-s">
{{#if (eq @policyType "acl")}}
<p data-test-example-modal-text="acl">
ACL Policies are written in Hashicorp Configuration Language (
<ExternalLink @href="https://github.com/hashicorp/hcl">HCL</ExternalLink>
) or JSON and describe which paths in Vault a user or machine is allowed to access. Here is an example policy:
</p>
{{else if (eq @policyType "rgp")}}
<p class="has-bottom-margin-s" data-test-example-modal-text="rgp">
Role Governing Policies (RGPs) are tied to client tokens or identities which is similar to
<DocLink @path="/vault/tutorials/policies/policies">ACL policies</DocLink>. They use
<DocLink @path="/vault/docs/enterprise/sentinel">Sentinel</DocLink>
as a language framework to enable fine-grained policy decisions.
</p>
<p>
Here is an example policy that uses RGP to restrict access to the
<code class="tag is-marginless is-paddingless">admin</code>
policy such that a user named "James Thomas" or has the
<code class="tag is-marginless is-paddingless">Team Lead</code>
role can manage the
<code class="tag is-marginless is-paddingless">admin</code>
policy:
</p>
{{else}}
<p class="has-bottom-margin-s" data-test-example-modal-text="egp">
Endpoint Governing Policies (EGPs) are tied to particular paths (e.g.
<code class="tag is-marginless is-paddingless">aws/creds/</code>
) instead of tokens. They use
<ExternalLink @href="https://docs.hashicorp.com/sentinel/language">Sentinel</ExternalLink>
as a language to access
<DocLink @path="/vault/docs/enterprise/sentinel/properties">properties</DocLink>
of the incoming requests.
</p>
<p>
Here is an example policy that fulfills the requirement of an incoming request to be performed during the business
hours 7:00am to 6:00pm on work days:
</p>
{{/if}}
</div>
<JsonEditor @value={{get this.policyTemplates @policyType}} @mode="ruby" @readOnly={{true}} @showToolbar={{true}} />
<div class="has-bottom-margin-m has-top-padding-s">
<p>
More information about
{{uppercase @policyType}}
policies can be found
<DocLink @path={{get this.moreInformationLinks @policyType}} data-test-example-modal-information-link>
here.
</DocLink>
</p>
</div>

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
/**
* @module PolicyExample
* The PolicyExample component receives a policy type ('acl', 'rgp', or 'egp') and renders a copyable policy example of
* that type using the <JsonEditor> component. Inside a modal, the PolicyExample component must be wrapped in a conditional
* (example below), otherwise the <JsonEditor> value won't render until it's focused.
*
* @example
* <PolicyExample
* @policyType={{@model.policyType}}
* />
*
* @example (in modal)
* <Modal
* @onClose={{fn (mut this.showTemplateModal) false}}
* @isActive={{this.showTemplateModal}}
* >
* <section class="modal-card-body">
* {{! code-mirror modifier does not render value initially until focus event fires }}
* {{! wait until the Modal is rendered and then show the PolicyExample (contains JsonEditor) }}
* {{#if this.showTemplateModal}}
* <PolicyExample @policyType={{@model.policyType}}/>
* {{/if}}
* </section>
* <div class="modal-card-head has-border-top-light">
* <button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
* Close
* </button>
* </div>
* </Modal>
* ```
* @param {string} policyType - policy type to decide which template to render; can either be "acl" or "rgp"
*/
export default class PolicyExampleComponent extends Component {
// formatting here is purposeful so that whitespace renders correctly in JsonEditor
policyTemplates = {
acl: `
# Grant 'create', 'read' , 'update', and list permission
# to paths prefixed by 'secret/*'
path "secret/*" {
capabilities = [ "create", "read", "update", "list" ]
}
# Even though we allowed secret/*, this line explicitly denies
# secret/super-secret. This takes precedence.
path "secret/super-secret" {
capabilities = ["deny"]
}
`,
rgp: `
# Import strings library that exposes common string operations
import "strings"
# Conditional rule (precond) checks the incoming request endpoint
# targeted to sys/policies/acl/admin
precond = rule {
strings.has_prefix(request.path, "sys/policies/admin")
}
# Vault checks to see if the request was made by an entity
# named James Thomas or Team Lead role defined as its metadata
main = rule when precond {
identity.entity.metadata.role is "Team Lead" or
identity.entity.name is "James Thomas"
}
`,
egp: `
import "time"
# Expect requests to only happen during work days (Monday
# through Friday) 0 for Sunday and 6 for Saturday
workdays = rule {
time.now.weekday > 0 and time.now.weekday < 6
}
# Expect requests to only happen during work hours (7:00 am -
# 6:00 pm)
workhours = rule {
time.now.hour > 7 and time.now.hour < 18
}
main = rule {
workdays and workhours
}
`,
};
moreInformationLinks = {
acl: '/vault/docs/concepts/policies#capabilities',
rgp: '/vault/tutorials/policies/sentinel#role-governing-policies-rgps',
egp: '/vault/docs/enterprise/sentinel#endpoint-governing-policies-egps',
};
}

View File

@@ -0,0 +1 @@
export { default } from 'core/components/policy-example';

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
const SELECTORS = {
policyText: '[data-test-modal-title]',
policyDescription: (type) => `[data-test-example-modal-text=${type}]`,
jsonText: '[data-test-example-modal-json-text]',
informationLink: '[data-test-example-modal-information-link]',
};
module('Integration | Component | policy-example', function (hooks) {
setupRenderingTest(hooks);
test('it renders the correct paragraph for ACL policy', async function (assert) {
await render(hbs`
<PolicyExample
@policyType="acl"
/>
`);
assert
.dom(SELECTORS.policyDescription('acl'))
.hasText(
'ACL Policies are written in Hashicorp Configuration Language ( HCL ) or JSON and describe which paths in Vault a user or machine is allowed to access. Here is an example policy:'
);
});
test('it renders the correct paragraph for RGP policy', async function (assert) {
await render(hbs`
<PolicyExample
@policyType="rgp"
/>
`);
assert
.dom(SELECTORS.policyDescription('rgp'))
.hasText(
'Role Governing Policies (RGPs) are tied to client tokens or identities which is similar to ACL policies . They use Sentinel as a language framework to enable fine-grained policy decisions.'
);
});
test('it renders the correct paragraph for EGP policy', async function (assert) {
await render(hbs`
<PolicyExample
@policyType="egp"
/>
`);
assert
.dom(SELECTORS.policyDescription('egp'))
.hasText(
`Endpoint Governing Policies (EGPs) are tied to particular paths (e.g. aws/creds/ ) instead of tokens. They use Sentinel as a language to access properties of the incoming requests.`
);
});
test('it renders the correct JSON editor text for ACL policy', async function (assert) {
await render(hbs`
<PolicyExample
@policyType="acl"
/>
`);
assert.dom(SELECTORS.jsonText).includesText(`# Grant 'create', 'read' , 'update', and list permission`);
});
test('it renders the correct JSON editor text for RGP policy', async function (assert) {
await render(hbs`
<PolicyExample
@policyType="rgp"
/>
`);
assert
.dom(SELECTORS.jsonText)
.includesText(`# Import strings library that exposes common string operations`);
});
test('it renders the correct JSON editor text for EGP policy', async function (assert) {
await render(hbs`
<PolicyExample
@policyType="egp"
/>
`);
assert.dom(SELECTORS.jsonText).includesText(`# Expect requests to only happen during work days (Monday `);
});
});

View File

@@ -18,6 +18,14 @@ const SELECTORS = {
saveButton: '[data-test-policy-save]',
cancelButton: '[data-test-policy-cancel]',
error: '[data-test-message-error]',
// For example modal:
exampleButton: '[data-test-policy-example-button]',
exampleModal: '[data-test-policy-example-modal]',
exampleModalTitle: '[data-test-modal-title]',
exampleModalClose: '[data-test-modal-close-button]',
// For additional fields for EGP policy:
fields: (name) => `[data-test-field=${name}]`,
pathsInput: (index) => `[data-test-string-list-input="${index}"]`,
};
module('Integration | Component | policy-form', function (hooks) {
@@ -42,6 +50,9 @@ module('Integration | Component | policy-form', function (hooks) {
this.put('/v1/sys/policies/rgp/**', () => {
return [204, { 'Content-Type': 'application/json' }];
});
this.put('/v1/sys/policies/egp/**', () => {
return [204, { 'Content-Type': 'application/json' }];
});
});
});
hooks.afterEach(function () {
@@ -102,6 +113,37 @@ module('Integration | Component | policy-form', function (hooks) {
assert.ok(this.onSave.calledOnceWith(this.model));
});
test('it renders the form for new EGP policy', async function (assert) {
const model = this.store.createRecord('policy/egp');
const policy = `
path "secret/*" {
capabilities = [ "create", "read", "update", "list" ]
}
`;
this.set('model', model);
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
assert.dom(SELECTORS.nameInput).exists({ count: 1 }, 'Name input exists');
assert.dom(SELECTORS.nameInput).hasNoText('Name field is not filled');
assert.dom(SELECTORS.uploadFileToggle).exists({ count: 1 }, 'Upload file toggle exists');
await fillIn(SELECTORS.nameInput, 'Foo');
assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input');
await fillIn(`${SELECTORS.policyEditor} textarea`, policy);
assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model');
assert.dom(SELECTORS.fields('paths')).exists('Paths field exists');
assert.dom(SELECTORS.pathsInput('0')).exists('0 field exists');
await fillIn(SELECTORS.pathsInput('0'), 'my path');
assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.saveButton).hasText('Create policy');
await click(SELECTORS.saveButton);
assert.ok(this.onSave.calledOnceWith(this.model));
});
test('it toggles to upload a new policy and uploads file', async function (assert) {
const policy = `
path "auth/token/lookup-self" {
@@ -156,6 +198,7 @@ module('Integration | Component | policy-form', function (hooks) {
await click(SELECTORS.saveButton);
assert.ok(this.onSave.calledOnceWith(this.model));
});
test('it renders the form to edit existing RGP policy', async function (assert) {
const model = this.store.createRecord('policy/rgp', {
name: 'bar',
@@ -185,6 +228,43 @@ module('Integration | Component | policy-form', function (hooks) {
await click(SELECTORS.saveButton);
assert.ok(this.onSave.calledOnceWith(this.model));
});
test('it renders the form to edit existing EGP policy', async function (assert) {
const model = this.store.createRecord('policy/egp', {
name: 'bar',
policy: 'some policy content',
paths: ['first path'],
});
model.save();
this.set('model', model);
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
assert.dom(SELECTORS.nameInput).doesNotExist('Name input is not rendered');
assert.dom(SELECTORS.uploadFileToggle).doesNotExist('Upload file toggle does not exist');
await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-');
assert.strictEqual(
this.model.policy,
'updated-some policy content',
'Policy editor updates policy value on model'
);
await fillIn(SELECTORS.pathsInput('1'), 'second path');
assert.strictEqual(
JSON.stringify(this.model.paths),
'["first path","second path"]',
'Second path field is updated on model'
);
assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.saveButton).hasText('Save', 'Save button text is correct');
await click(SELECTORS.saveButton);
assert.ok(this.onSave.calledOnceWith(this.model));
});
test('it shows the error message on form when save fails', async function (assert) {
const model = this.store.createRecord('policy/acl', {
name: 'bad-policy',
@@ -203,4 +283,138 @@ module('Integration | Component | policy-form', function (hooks) {
assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.error).includesText('An error occurred');
});
test('it does not create a new policy when the cancel button is clicked', async function (assert) {
const policy = `
path "secret/*" {
capabilities = [ "create", "read", "update", "list" ]
}
`;
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
await fillIn(SELECTORS.nameInput, 'Foo');
assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input');
await fillIn(`${SELECTORS.policyEditor} textarea`, policy);
assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model');
await click(SELECTORS.cancelButton);
assert.ok(this.onSave.notCalled);
assert.ok(this.onCancel.calledOnce, 'Form calls onCancel');
});
test('it does not save edits when the cancel button is clicked', async function (assert) {
const model = this.store.createRecord('policy/acl', {
name: 'foo',
policy: 'some policy content',
});
model.save();
this.set('model', model);
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-');
assert.strictEqual(
this.model.policy,
'updated-some policy content',
'Policy editor updates policy value on model'
);
await click(SELECTORS.cancelButton);
assert.ok(this.onSave.notCalled);
assert.ok(this.onCancel.calledOnce, 'Form calls onCancel');
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
assert.strictEqual(
this.model.policy,
'some policy content',
'Policy editor shows original policy content, meaning that onCancel worked successfully'
);
});
test('it does not render the button and modal for the policy example if not specified to', async function (assert) {
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
assert.dom(SELECTORS.exampleModal).doesNotExist('Modal for the policy example does not exist');
assert.dom(SELECTORS.exampleButton).doesNotExist('Button for the policy example modal does not exist');
});
test('it renders the button and modal for the policy example when specified to', async function (assert) {
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@renderPolicyExampleModal={{true}}
/>
<div id="modal-wormhole"></div>
`);
assert.dom(SELECTORS.exampleButton).exists({ count: 1 }, 'Modal for the policy example exists');
assert.dom(SELECTORS.exampleButton).exists({ count: 1 }, 'Button for the policy example modal exists');
});
test('it renders the correct title for ACL example for the policy example modal', async function (assert) {
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@renderPolicyExampleModal={{true}}
/>
<div id="modal-wormhole"></div>
`);
await click(SELECTORS.exampleButton);
assert.dom(SELECTORS.exampleModalTitle).hasText('Example ACL Policy');
});
test('it renders the correct title for RGP example for the policy example modal', async function (assert) {
const model = this.store.createRecord('policy/rgp');
this.set('model', model);
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@renderPolicyExampleModal={{true}}
/>
<div id="modal-wormhole"></div>
`);
await click(SELECTORS.exampleButton);
assert.dom(SELECTORS.exampleModalTitle).hasText('Example RGP Policy');
});
test('it renders the correct title for EGP example for the policy example modal', async function (assert) {
const model = this.store.createRecord('policy/egp');
this.set('model', model);
await render(hbs`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@renderPolicyExampleModal={{true}}
/>
<div id="modal-wormhole"></div>
`);
await click(SELECTORS.exampleButton);
assert.dom(SELECTORS.exampleModalTitle).hasText('Example EGP Policy');
});
});