Added non-Stripe members setting screen acceptance tests

refs https://github.com/TryGhost/Team/issues/1358

- added acceptance tests for members settings screen
  - subscription access management
  - default post access management
  - free tier management
- fixed `enableLabsFlag()` test helper overwriting existing flag settings when enabling another one
- updated API mocks and fixtures
  - matched product fixtures to default tiers-enabled products
  - updated product API mocks to include benefit handling
This commit is contained in:
Kevin Ansfield 2022-02-18 22:36:01 +00:00
parent 8af0ce7474
commit 4b646d40ea
22 changed files with 505 additions and 89 deletions

View File

@ -1831,3 +1831,8 @@ remove|ember-template-lint|no-action|7|12|7|12|fada170dae44678cba8240b4ae3233c63
remove|ember-template-lint|no-action|17|12|17|12|b75d66d02a33b108a64bb94d494fa194434c82f9|1644364800000|1646956800000|1649545200000|lib/koenig-editor/addon/components/koenig-toolbar.hbs remove|ember-template-lint|no-action|17|12|17|12|b75d66d02a33b108a64bb94d494fa194434c82f9|1644364800000|1646956800000|1649545200000|lib/koenig-editor/addon/components/koenig-toolbar.hbs
remove|ember-template-lint|no-action|28|16|28|16|e5e787868d089ae3141666004a914deff774b489|1644364800000|1646956800000|1649545200000|lib/koenig-editor/addon/components/koenig-toolbar.hbs remove|ember-template-lint|no-action|28|16|28|16|e5e787868d089ae3141666004a914deff774b489|1644364800000|1646956800000|1649545200000|lib/koenig-editor/addon/components/koenig-toolbar.hbs
remove|ember-template-lint|no-action|70|12|70|12|03d2eaf613eae8cb3ea3c821ba63544c730364c8|1644364800000|1646956800000|1649545200000|lib/koenig-editor/addon/components/koenig-toolbar.hbs remove|ember-template-lint|no-action|70|12|70|12|03d2eaf613eae8cb3ea3c821ba63544c730364c8|1644364800000|1646956800000|1649545200000|lib/koenig-editor/addon/components/koenig-toolbar.hbs
remove|ember-template-lint|no-duplicate-landmark-elements|126|24|126|24|a19c12d5f0d5fa9b890943214b862502d9f2dcee|1643760000000|1646352000000|1648940400000|app/components/modal-product.hbs
remove|ember-template-lint|no-nested-landmark|126|24|126|24|a19c12d5f0d5fa9b890943214b862502d9f2dcee|1643760000000|1646352000000|1648940400000|app/components/modal-product.hbs
remove|ember-template-lint|no-duplicate-attributes|9|4|9|4|6b5f76f812df2b84f2ed9ee5a557ca1bf98710df|1643760000000|1646352000000|1648940400000|app/components/gh-post-settings-menu/visibility-segment-select.hbs
add|ember-template-lint|no-duplicate-landmark-elements|131|24|131|24|9eb7d301f1f50334e793aafab8f6b9e8905125ab|1645142400000|1647734400000|1650322800000|app/components/modal-product.hbs
add|ember-template-lint|no-nested-landmark|131|24|131|24|9eb7d301f1f50334e793aafab8f6b9e8905125ab|1645142400000|1647734400000|1650322800000|app/components/modal-product.hbs

View File

@ -21,19 +21,22 @@
@input={{action "updateLabel" value="target.value"}} @input={{action "updateLabel" value="target.value"}}
@keyPress={{action "clearLabelErrors"}} @keyPress={{action "clearLabelErrors"}}
@stopEnterKeyDownPropagation={{true}} @stopEnterKeyDownPropagation={{true}}
@focus-out={{action "updateLabel" this.name}} data-test-input="name" /> @focus-out={{action "updateLabel" this.name}}
data-test-input="benefit-label" />
<GhErrorMessage <GhErrorMessage
@errors={{this.benefitItem.errors}} @errors={{this.benefitItem.errors}}
@property="name" data-test-error="name" /> @property="name"
data-test-error="benefit-label" />
</GhValidationStatusContainer> </GhValidationStatusContainer>
</div> </div>
{{#if this.benefitItem.isNew}} {{#if this.benefitItem.isNew}}
<button type="button" class="gh-blognav-add" {{action "addItem" this.benefitItem}}> <button type="button" class="gh-blognav-add" {{action "addItem" this.benefitItem}} data-test-button="add-benefit">
{{svg-jar "add"}}<span class="sr-only">Add</span> {{svg-jar "add"}}<span class="sr-only">Add</span>
</button> </button>
{{else}} {{else}}
<button type="button" class="gh-blognav-delete" {{action "deleteItem" this.benefitItem}}> <button type="button" class="gh-blognav-delete" {{action "deleteItem" this.benefitItem}} data-test-button="delete-benefit">
{{svg-jar "trash"}}<span class="sr-only">Delete</span> {{svg-jar "trash"}}<span class="sr-only">Delete</span>
</button> </button>
{{/if}} {{/if}}

View File

@ -6,12 +6,11 @@
@allowCreation={{false}} @allowCreation={{false}}
@renderInPlace={{this.renderInPlace}} @renderInPlace={{this.renderInPlace}}
@onChange={{this.setSegment}} @onChange={{this.setSegment}}
@disabled={{@disabled}}
@class="select-members" @class="select-members"
@placeholder="Select a tier" @placeholder="Select a tier"
as |option| as |option|
> >
{{option.name}} <span data-test-visibility-segment-option={{option.name}}>{{option.name}}</span>
</GhTokenInput> </GhTokenInput>
<GhMembersSegmentCount <GhMembersSegmentCount

View File

@ -1,21 +1,21 @@
<div class="gh-main-content-card gh-product-card"> <div class="gh-main-content-card gh-product-card" data-test-product-card={{@product.slug}}>
<div class="gh-product-card-block title-block"> <div class="gh-product-card-block title-block">
<h3 class="gh-product-card-name"> <h3 class="gh-product-card-name" data-test-name>
{{this.product.name}} {{@product.name}}
</h3> </h3>
<p class="gh-product-card-description"> <p class="gh-product-card-description" data-test-description>
{{#if this.product.description.length}} {{#if @product.description.length}}
{{this.product.description}} {{@product.description}}
{{else}} {{else}}
No description added for this tier. No description added for this tier.
{{/if}} {{/if}}
</p> </p>
</div> </div>
<div class="gh-product-card-block benefits-block"> <div class="gh-product-card-block benefits-block" data-test-benefits>
<h4>Benefits <span class="counter">({{if this.product.benefits.length this.product.benefits.length "0"}})</span></h4> <h4>Benefits <span class="counter">({{or @product.benefits.length "0"}})</span></h4>
{{#if this.product.benefits.length}} {{#if @product.benefits.length}}
<ul class="benefits"> <ul class="benefits">
{{#each this.product.benefits as |benefit|}} {{#each @product.benefits as |benefit|}}
<li>{{svg-jar "check"}} {{benefit.name}} </li> <li>{{svg-jar "check"}} {{benefit.name}} </li>
{{/each}} {{/each}}
</ul> </ul>
@ -23,10 +23,10 @@
<p class="gh-product-card-description">No benefits added for this tier.</p> <p class="gh-product-card-description">No benefits added for this tier.</p>
{{/if}} {{/if}}
</div> </div>
{{#if (eq this.product.type "free" )}} {{#if (eq @product.type "free" )}}
<div class="gh-product-card-block"> <div class="gh-product-card-block">
<div class="gh-product-price-container"> <div class="gh-product-price-container">
<div class="gh-product-card-price"> <div class="gh-product-card-price" data-test-free-price>
<div class="flex items-start"> <div class="flex items-start">
<span class="currency">{{currency-symbol this.productCurrency}}</span> <span class="currency">{{currency-symbol this.productCurrency}}</span>
<span class="amount">0</span> <span class="amount">0</span>
@ -35,31 +35,31 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if (eq this.product.type "paid" )}} {{#if (eq @product.type "paid" )}}
<div class="gh-product-card-block"> <div class="gh-product-card-block">
<div class="gh-product-price-container"> <div class="gh-product-price-container">
<div class="gh-product-card-price"> <div class="gh-product-card-price" data-test-monthly-price>
<div class="flex items-start"> <div class="flex items-start">
<span class="currency">{{currency-symbol this.productCurrency}}</span> <span class="currency">{{currency-symbol this.productCurrency}}</span>
<span class="amount">{{gh-price-amount this.product.monthlyPrice.amount}}</span> <span class="amount">{{gh-price-amount @product.monthlyPrice.amount}}</span>
</div> </div>
<div class="period">Monthly</div> <div class="period">Monthly</div>
</div> </div>
<div class="gh-product-card-price"> <div class="gh-product-card-price" data-test-yearly-price>
<div class="flex items-start"> <div class="flex items-start">
<span class="currency">{{currency-symbol this.productCurrency}}</span> <span class="currency">{{currency-symbol this.productCurrency}}</span>
<span class="amount">{{gh-price-amount this.product.yearlyPrice.amount}}</span> <span class="amount">{{gh-price-amount @product.yearlyPrice.amount}}</span>
</div> </div>
<div class="period">Yearly</div> <div class="period">Yearly</div>
</div> </div>
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if (eq this.product.type "paid" )}} {{#if (eq @product.type "paid" )}}
<div class="gh-product-card-button-container"> <div class="gh-product-card-button-container">
<span class="dropdown"> <span class="dropdown">
<GhDropdownButton <GhDropdownButton
@dropdownName="tiers-actions-menu-{{this.product.name}}" @dropdownName="tiers-actions-menu-{{@product.name}}"
@classNames="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-actions-button icon-only" @classNames="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-actions-button icon-only"
@title="Tiers Actions" @title="Tiers Actions"
data-test-button="tiers-actions" data-test-button="tiers-actions"
@ -70,19 +70,19 @@
</span> </span>
</GhDropdownButton> </GhDropdownButton>
<GhDropdown <GhDropdown
@name="tiers-actions-menu-{{this.product.name}}" @name="tiers-actions-menu-{{@product.name}}"
@tagName="ul" @tagName="ul"
@classNames="gh-tier-actions-menu dropdown-menu dropdown-triangle-top-right" @classNames="gh-tier-actions-menu dropdown-menu dropdown-triangle-top-right"
> >
<li> <li>
<button class="mr2" type="button" {{on "click" (fn this.openEditProduct this.product)}}> <button class="mr2" type="button" {{on "click" (fn this.openEditProduct @product)}}>
<span>Edit</span> <span>Edit</span>
</button> </button>
</li> </li>
{{#if this.showArchiveOption}} {{#if this.showArchiveOption}}
<li> <li>
<Settings::Members::ArchiveTier <Settings::Members::ArchiveTier
@product={{this.product}} @product={{@product}}
@onUnarchive={{@onUnarchive}} @onUnarchive={{@onUnarchive}}
/> />
</li> </li>
@ -92,7 +92,7 @@
</div> </div>
{{else}} {{else}}
<div class="gh-product-card-button-container"> <div class="gh-product-card-button-container">
<button type="button" {{on "click" (fn this.openEditProduct this.product)}} class="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-edit-button icon-only"> <button type="button" {{on "click" (fn this.openEditProduct @product)}} class="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-edit-button icon-only" data-test-button="edit-product">
<span> <span>
{{svg-jar "pen"}} {{svg-jar "pen"}}
</span> </span>

View File

@ -12,7 +12,6 @@ export default class extends Component {
@service config; @service config;
@tracked showProductModal = false; @tracked showProductModal = false;
@tracked productModel = null;
get product() { get product() {
return this.args.product; return this.args.product;

View File

@ -4,5 +4,6 @@
@input={{this.setValue}} @input={{this.setValue}}
@focus-out={{this.validateUrlInput}} @focus-out={{this.validateUrlInput}}
@placeholder={{this.placeholder}} @placeholder={{this.placeholder}}
...attributes
/> />

View File

@ -2,8 +2,8 @@
{{svg-jar "close"}} {{svg-jar "close"}}
</button> </button>
<div class="gh-product-modal-content"> <div class="gh-product-modal-content" data-test-modal="edit-product">
<header class="modal-header" data-test-modal="webhook-form"> <header class="modal-header">
<h1 data-test-text="title">{{this.title}}</h1> <h1 data-test-text="title">{{this.title}}</h1>
</header> </header>
@ -14,7 +14,7 @@
<h4 class="gh-main-section-header small bn">Basic</h4> <h4 class="gh-main-section-header small bn">Basic</h4>
<div class="gh-main-section-content grey gh-product-priceform-block"> <div class="gh-main-section-content grey gh-product-priceform-block">
{{#unless this.isFreeProduct}} {{#unless this.isFreeProduct}}
<GhFormGroup @errors={{this.errors}} @property="name"> <GhFormGroup @errors={{this.errors}} @property="name" data-test-formgroup="name">
<label for="name" class="fw6">Name</label> <label for="name" class="fw6">Name</label>
<GhTextInput <GhTextInput
@value={{readonly this.product.name}} @value={{readonly this.product.name}}
@ -22,11 +22,13 @@
@name="name" @name="name"
@placeholder="Bronze" @placeholder="Bronze"
@id="name" @id="name"
@class="gh-input" /> @class="gh-input"
data-test-input="product-name" />
<GhErrorMessage @errors={{this.errors}} @property="name" /> <GhErrorMessage @errors={{this.errors}} @property="name" />
</GhFormGroup> </GhFormGroup>
{{/unless}} {{/unless}}
<GhFormGroup @errors={{this.errors}} @property="description">
<GhFormGroup @errors={{this.errors}} @property="description" data-test-formgroup="description">
<label for="description" class="fw6">Description</label> <label for="description" class="fw6">Description</label>
{{#if this.isFreeProduct}} {{#if this.isFreeProduct}}
<GhTextInput <GhTextInput
@ -35,7 +37,8 @@
@name="description" @name="description"
@placeholder="Free preview of {{this.settings.title}}" @placeholder="Free preview of {{this.settings.title}}"
@id="description" @id="description"
@class="gh-input" /> @class="gh-input"
data-test-input="free-product-description" />
{{else}} {{else}}
<GhTextInput <GhTextInput
@value={{readonly this.product.description}} @value={{readonly this.product.description}}
@ -43,12 +46,14 @@
@name="description" @name="description"
@placeholder="Full access to premium content" @placeholder="Full access to premium content"
@id="description" @id="description"
@class="gh-input" /> @class="gh-input"
data-test-input="product-description" />
{{/if}} {{/if}}
<GhErrorMessage @errors={{this.errors}} @property="description" /> <GhErrorMessage @errors={{this.errors}} @property="description" />
</GhFormGroup> </GhFormGroup>
{{#unless this.isFreeProduct}} {{#unless this.isFreeProduct}}
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices"> <GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="prices" data-test-formgroup="prices">
<div class="gh-settings-members-pricelabelcont"> <div class="gh-settings-members-pricelabelcont">
<label for="monthlyPrice">Prices</label> <label for="monthlyPrice">Prices</label>
<span></span> <span></span>
@ -138,7 +143,7 @@
@focusItem={{action "focusItem"}} @focusItem={{action "focusItem"}}
@deleteItem={{action "deleteBenefit"}} @deleteItem={{action "deleteBenefit"}}
@updateLabel={{action "updateLabel"}} @updateLabel={{action "updateLabel"}}
data-test-navitem={{index}} /> data-test-benefit-item={{index}} />
</DraggableObject> </DraggableObject>
{{/each}} {{/each}}
</SortableObjects> </SortableObjects>
@ -148,45 +153,45 @@
@addItem={{action "addBenefit"}} @addItem={{action "addBenefit"}}
@deleteItem={{action "deleteBenefit"}} @deleteItem={{action "deleteBenefit"}}
@updateLabel={{action "updateLabel"}} @updateLabel={{action "updateLabel"}}
data-test-navitem="new" /> data-test-benefit-item="new" />
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="gh-main-section-block gh-product-form-tierpreview"> <div class="gh-main-section-block gh-product-form-tierpreview" data-test-tierpreview>
<div class="gh-product-form-tierpreview-content"> <div class="gh-product-form-tierpreview-content">
{{#if this.isFreeProduct}} {{#if this.isFreeProduct}}
<h4 class="gh-main-section-header small bn">Free Membership Preview</h4> <h4 class="gh-main-section-header small bn" data-test-tierpreview-title>Free Membership Preview</h4>
{{else}} {{else}}
<h4 class="gh-main-section-header small bn">Tier Preview</h4> <h4 class="gh-main-section-header small bn" data-test-tierpreview-title>Tier Preview</h4>
{{/if}} {{/if}}
<div class="gh-main-section-content" style="border-color: {{this.settings.accentColor}}"> <div class="gh-main-section-content" style="border-color: {{this.settings.accentColor}}">
<span class="checkmark" style="background-color: {{this.settings.accentColor}}"></span> <span class="checkmark" style="background-color: {{this.settings.accentColor}}"></span>
{{#if this.product.name}} {{#if this.product.name}}
<h4>{{this.product.name}}</h4> <h4 data-test-tierpreview-name>{{this.product.name}}</h4>
{{else}} {{else}}
<h4 class="placeholder">Bronze</h4> <h4 class="placeholder" data-test-tierpreview-name>Bronze</h4>
{{/if}} {{/if}}
{{#if this.product.description}} {{#if this.product.description}}
<p>{{this.product.description}}</p> <p data-test-tierpreview-description>{{this.product.description}}</p>
{{else}} {{else}}
{{#if this.isFreeProduct}} {{#if this.isFreeProduct}}
<p class="placeholder">Free preview of {{this.settings.title}}</p> <p class="placeholder" data-test-tierpreview-description>Free preview of {{this.settings.title}}</p>
{{else}} {{else}}
<p class="placeholder">Full access to premium content</p> <p class="placeholder" data-test-tierpreview-description>Full access to premium content</p>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if this.benefits}} {{#if this.benefits}}
<ul> <ul data-test-tierpreview-benefits>
{{#each this.benefits as |benefitItem index|}} {{#each this.benefits as |benefitItem index|}}
<li>{{svg-jar "check-2"}} <span>{{benefitItem.name}}</span></li> <li>{{svg-jar "check-2"}} <span>{{benefitItem.name}}</span></li>
{{/each}} {{/each}}
</ul> </ul>
{{else}} {{else}}
<ul class="placeholder"> <ul class="placeholder" data-test-tierpreview-benefits>
{{#if this.isFreeProduct}} {{#if this.isFreeProduct}}
<li>{{svg-jar "check-2"}} <span>Access to all public posts</span></li> <li>{{svg-jar "check-2"}} <span>Access to all public posts</span></li>
{{else}} {{else}}
@ -194,12 +199,12 @@
{{/if}} {{/if}}
</ul> </ul>
{{/if}} {{/if}}
<div class="price"> <div class="price" data-test-tierpreview-price>
{{#if this.isFreeProduct}} {{#if this.isFreeProduct}}
<span class="monthly-price"> <span class="monthly-price">
<span class="currency">{{currency-symbol this.currency}}</span> <span class="currency">{{currency-symbol this.currency}}</span>
0 0
</span> </span>
{{else}} {{else}}
{{#if this.stripeMonthlyAmount}} {{#if this.stripeMonthlyAmount}}
<span class="monthly-price"> <span class="monthly-price">

View File

@ -1,4 +1,4 @@
<div class="mb4 gh-setting-large-dropdown"> <div class="mb4 gh-setting-large-dropdown" data-test-default-post-access>
<div class="gh-expandable-header"> <div class="gh-expandable-header">
<div> <div>
<h4 class="gh-expandable-title">Default post access</h4> <h4 class="gh-expandable-title">Default post access</h4>
@ -15,7 +15,7 @@
@dropdownClass="gh-setting-dropdown-list" @dropdownClass="gh-setting-dropdown-list"
as |option| as |option|
> >
<div class="gh-setting-dropdown-content"> <div class="gh-setting-dropdown-content" data-test-default-post-access-option={{option.value}}>
{{svg-jar option.icon class=(concat "w8 h8 mr2 fill-" (or option.icon_color "green"))}} {{svg-jar option.icon class=(concat "w8 h8 mr2 fill-" (or option.icon_color "green"))}}
<div class="gh-radio-label"> <div class="gh-radio-label">
{{option.name}}<br> {{option.name}}<br>
@ -24,7 +24,7 @@
</div> </div>
</PowerSelect> </PowerSelect>
{{#if this.hasVisibilityFilter}} {{#if this.hasVisibilityFilter}}
<div class="mt2"> <div class="mt2" data-test-default-post-access-tiers>
<GhPostSettingsMenu::VisibilitySegmentSelect <GhPostSettingsMenu::VisibilitySegmentSelect
@selectDefaultProduct={{true}} @selectDefaultProduct={{true}}
@tiers={{this.visibilityTiers}} @tiers={{this.visibilityTiers}}

View File

@ -46,7 +46,7 @@ export default class SettingsMembersDefaultPostAccess extends Component {
get visibilityTiers() { get visibilityTiers() {
const visibilityTiersData = this.settings.get('defaultContentVisibilityTiers'); const visibilityTiersData = this.settings.get('defaultContentVisibilityTiers');
return visibilityTiersData.map((id) => { return (visibilityTiersData || []).map((id) => {
return {id}; return {id};
}); });
} }

View File

@ -1,4 +1,4 @@
<div class="gh-setting-richdd-container"> <div class="gh-setting-richdd-container" data-test-members-subscription-access>
<div class="gh-expandable-header"> <div class="gh-expandable-header">
<div> <div>
<h4 class="gh-expandable-title">Subscription access</h4> <h4 class="gh-expandable-title">Subscription access</h4>
@ -14,7 +14,7 @@
@dropdownClass="gh-setting-dropdown-list" @dropdownClass="gh-setting-dropdown-list"
as |option| as |option|
> >
<div class="gh-setting-dropdown-content"> <div class="gh-setting-dropdown-content" data-test-members-subscription-option={{option.value}}>
{{svg-jar option.icon class=(concat "w8 h8 mr2 fill-" (or option.icon_color "green"))}} {{svg-jar option.icon class=(concat "w8 h8 mr2 fill-" (or option.icon_color "green"))}}
<div class="gh-radio-label"> <div class="gh-radio-label">
{{option.name}}<br> {{option.name}}<br>

View File

@ -50,7 +50,7 @@
<div class="gh-setting-members-portalpreview"> <div class="gh-setting-members-portalpreview">
<div class="gh-setting-members-portal-mock {{if (feature "multipleProducts") "mock-enabled"}}"> <div class="gh-setting-members-portal-mock {{if (feature "multipleProducts") "mock-enabled"}}">
{{#if (or (eq this.settings.membersSignupAccess 'none') this.switchFromNoneTask.isRunning)}} {{#if (or (eq this.settings.membersSignupAccess 'none') this.switchFromNoneTask.isRunning)}}
<div class="gh-setting-members-portal-disabled"> <div class="gh-setting-members-portal-disabled" data-test-portal-preview-disabled>
<span class="lightgrey">{{svg-jar "portal-logo-stroke"}}</span> <span class="lightgrey">{{svg-jar "portal-logo-stroke"}}</span>
<h4>Portal disabled</h4> <h4>Portal disabled</h4>
<p>Change your Subscription Access setting to re-enable Portal</p> <p>Change your Subscription Access setting to re-enable Portal</p>
@ -61,7 +61,8 @@
@src={{this.portalPreviewUrl}} @src={{this.portalPreviewUrl}}
@invisibleUntilLoaded="portal-ready" @invisibleUntilLoaded="portal-ready"
@onInserted={{this.portalPreviewInserted}} @onInserted={{this.portalPreviewInserted}}
@onDestroyed={{this.portalPreviewDestroyed}} /> @onDestroyed={{this.portalPreviewDestroyed}}
data-test-iframe="portal-preview"/>
{{/if}} {{/if}}
</div> </div>
</div> </div>
@ -85,11 +86,11 @@
<h4 class="gh-expandable-title">Free</h4> <h4 class="gh-expandable-title">Free</h4>
<p class="gh-expandable-description">Free member sign up settings</p> <p class="gh-expandable-description">Free member sign up settings</p>
</div> </div>
<button type="button" class="gh-btn" {{on "click" (toggle "freeOpen" this)}} data-test-toggle-pub-info><span>{{if this.freeOpen "Close" "Expand"}}</span></button> <button type="button" class="gh-btn" {{on "click" (toggle "freeOpen" this)}} data-test-button="toggle-free-settings"><span>{{if this.freeOpen "Close" "Expand"}}</span></button>
</div> </div>
<div class="gh-expandable-content"> <div class="gh-expandable-content">
{{#liquid-if this.freeOpen}} {{#liquid-if this.freeOpen}}
<div class="gh-setting-content-extended"> <div class="gh-setting-content-extended" data-test-free-settings-expanded>
{{#if (feature "multipleProducts")}} {{#if (feature "multipleProducts")}}
<GhProductCard <GhProductCard
@product={{this.freeProduct}} @product={{this.freeProduct}}
@ -107,6 +108,7 @@
@setResult={{this.setFreeSignupRedirect}} @setResult={{this.setFreeSignupRedirect}}
@validateUrl={{this.validateFreeSignupRedirect}} @validateUrl={{this.validateFreeSignupRedirect}}
@placeholder={{readonly this.siteUrl}} @placeholder={{readonly this.siteUrl}}
data-test-input="old-free-welcome-page"
/> />
<GhErrorMessage <GhErrorMessage
@errors={{this.settings.errors}} @errors={{this.settings.errors}}
@ -124,6 +126,7 @@
@setResult={{this.setWelcomePageURL}} @setResult={{this.setWelcomePageURL}}
@validateUrl={{this.validateWelcomePageURL}} @validateUrl={{this.validateWelcomePageURL}}
@placeholder={{readonly this.siteUrl}} @placeholder={{readonly this.siteUrl}}
data-test-input="free-welcome-page"
/> />
<p>Redirect to this URL after signup for a free membership</p> <p>Redirect to this URL after signup for a free membership</p>
</GhFormGroup> </GhFormGroup>

View File

@ -21,7 +21,28 @@ export default function mockProducts(server) {
}); });
}); });
server.put('/products/:id/'); server.put('/products/:id/', function ({products, productBenefits}, {params}) {
const attrs = this.normalizedRequestAttrs();
const product = products.find(params.id);
const benefitAttrs = attrs.benefits;
delete attrs.benefits;
product.update(attrs);
benefitAttrs.forEach((benefit) => {
if (benefit.id) {
const productBenefit = productBenefits.find(benefit.id);
productBenefit.product = product;
productBenefit.save();
} else {
product.createProductBenefit(benefit);
product.save();
}
});
return product.save();
});
server.del('/products/:id/'); server.del('/products/:id/');
} }

View File

@ -0,0 +1,25 @@
import {Factory} from 'ember-cli-mirage';
export default Factory.extend({
name(i) { return `Product ${i}`; },
description(i) { return `Description for product ${i}`; },
active: true,
slug(i) { return `product-${i}`;},
type: 'paid',
monthly_price() {
return {
interval: 'month',
nickname: 'Monthly',
currency: 'usd',
amount: 500
};
},
yearly_price() {
return {
interval: 'year',
nickname: 'Yearly',
currency: 'usd',
amount: 5000
};
}
});

View File

@ -1,24 +1,31 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
export default [ export default [
{ {
id: 1, id: '1',
name: 'Test Product', active: true,
slug: 'test-product', benefits: [],
monthly_price: { createdAt: '2022-02-04T13:11:40.000Z',
interval: 'month', description: null,
nickname: 'Monthly', monthlyPrice: null,
currency: 'usd', name: 'Free',
amount: 500 slug: 'free',
}, type: 'free',
yearly_price: { updatedAt: '2022-02-04T13:34:53.000Z',
interval: 'year', welcomePageUrl: null,
nickname: 'Yearly', yearlyPrice: null
currency: 'usd', },
amount: 5000 {
}, id: '2',
created_at: '2015-11-13T16:01:29.131Z', active: true,
created_by: 1, benefits: [],
updated_at: '2015-11-13T16:01:29.131Z', createdAt: '2022-02-04T13:11:40.000Z',
updated_by: 1 description: null,
monthlyPrice: null,
name: 'Default Product',
slug: 'default-product',
type: 'paid',
updatedAt: '2022-02-04T13:11:40.000Z',
welcomePageUrl: null,
yearlyPrice: null
} }
]; ];

View File

@ -222,5 +222,35 @@ export default [
updated_at: '2021-11-01T15:44:43.494Z', updated_at: '2021-11-01T15:44:43.494Z',
updated_by: 1, updated_by: 1,
value: 'casper' value: 'casper'
},
{
id: 28,
created_at: '2022-02-16T09:38:00.000Z',
created_by: 1,
key: 'members_signup_access',
group: 'members',
updated_at: '2022-02-16T09:38:00.000Z',
updated_by: 1,
value: 'all'
},
{
id: 29,
created_at: '2022-02-16T09:38:00.000Z',
created_by: 1,
key: 'default_content_visibility',
group: 'members',
updated_at: '2022-02-16T09:38:00.000Z',
updated_by: 1,
value: 'public'
},
{
id: 30,
created_at: '2022-02-16T09:38:00.000Z',
created_by: 1,
key: 'default_content_visibility_tiers',
group: 'members',
updated_at: '2022-02-16T09:38:00.000Z',
updated_by: 1,
value: '[]'
} }
]; ];

View File

@ -0,0 +1,5 @@
import {Model, belongsTo} from 'ember-cli-mirage';
export default Model.extend({
product: belongsTo('product')
});

View File

@ -1,4 +1,7 @@
import {Model} from 'ember-cli-mirage'; import {Model, hasMany} from 'ember-cli-mirage';
export default Model.extend({ export default Model.extend({
// ran into odd relationship bugs when called `benefits`
// serializer will rename to `benefits`
productBenefits: hasMany()
}); });

View File

@ -0,0 +1,3 @@
import BaseSerializer from './application';
export default BaseSerializer.extend({});

View File

@ -0,0 +1,22 @@
import BaseSerializer from './application';
import {underscore} from '@ember/string';
export default BaseSerializer.extend({
embed: true,
include(/*request*/) {
let includes = [];
includes.push('productBenefits');
return includes;
},
keyForEmbeddedRelationship(relationshipName) {
if (relationshipName === 'productBenefits') {
return 'benefits';
}
return underscore(relationshipName);
}
});

View File

@ -42,8 +42,9 @@ describe('Acceptance: Offers', function () {
}); });
it('it renders, can be navigated, can edit offer', async function () { it('it renders, can be navigated, can edit offer', async function () {
let offer1 = this.server.create('offer', {createdAt: moment.utc().subtract(1, 'day').valueOf()}); const product = this.server.create('product');
this.server.create('offer', {createdAt: moment.utc().subtract(2, 'day').valueOf()}); let offer1 = this.server.create('offer', {tier: {id: product.id}, createdAt: moment.utc().subtract(1, 'day').valueOf()});
this.server.create('offer', {tier: {id: product.id}, createdAt: moment.utc().subtract(2, 'day').valueOf()});
await visit('/offers'); await visit('/offers');

View File

@ -0,0 +1,283 @@
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {blur, click, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {enableLabsFlag} from '../../helpers/labs-flag';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {setupMirage} from 'ember-cli-mirage/test-support';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Membership', function () {
const hooks = setupApplicationTest();
setupMirage(hooks);
beforeEach(function () {
enableLabsFlag(this.server, 'multipleProducts');
enableLabsFlag(this.server, 'tierWelcomePages');
enableLabsFlag(this.server, 'tierName');
});
beforeEach(async function () {
this.server.loadFixtures('configs');
this.server.loadFixtures('products');
const role = this.server.create('role', {name: 'Owner'});
this.server.create('user', {roles: [role]});
return await authenticateSession();
});
describe('permissions', function () {
let visitAs;
before(function () {
visitAs = async (roleName) => {
const role = this.server.create('role', {name: roleName});
this.server.create('user', {roles: [role]});
await authenticateSession();
await visit('/settings/members');
};
});
beforeEach(async function () {
this.server.db.users.remove();
await invalidateSession();
});
it('allows Owners', async function () {
await visitAs('Owner');
expect(currentURL()).to.equal('/settings/members');
});
it('allows Administrators', async function () {
await visitAs('Administrator');
expect(currentURL()).to.equal('/settings/members');
});
it('disallows Editors', async function () {
await visitAs('Editor');
expect(currentURL()).to.not.equal('/settings/members');
});
it('disallows Authors', async function () {
await visitAs('Author');
expect(currentURL()).to.not.equal('/settings/members');
});
it('disallows Contributors', async function () {
await visitAs('Contributor');
expect(currentURL()).to.not.equal('/settings/members');
});
});
it('can change subscription access', async function () {
await visit('/settings/members');
expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('all');
expect(find('[data-test-members-subscription-option="all"]'), 'initial selection is "all"').to.exist;
expect(find('[data-test-iframe="portal-preview"]'), 'initial preview src matches "all"')
.to.have.attribute('src').match(/membersSignupAccess=all/);
// open dropdown
await click('[data-test-members-subscription-option="all"]');
// all settings exist in dropdown
expect(find('.ember-power-select-options [data-test-members-subscription-option="all"]'), 'all option').to.exist;
expect(find('.ember-power-select-options [data-test-members-subscription-option="invite"]'), 'invite option').to.exist;
expect(find('.ember-power-select-options [data-test-members-subscription-option="none"]'), 'none option').to.exist;
// switch to invite
await click('.ember-power-select-options [data-test-members-subscription-option="invite"]');
expect(find('.ember-power-select-options'), 'dropdown closes').to.not.exist;
expect(find('[data-test-members-subscription-option="invite"]'), 'invite option shown after selected').to.exist;
expect(find('[data-test-iframe="portal-preview"]'))
.to.have.attribute('src').match(/membersSignupAccess=invite/);
await click('[data-test-button="save-settings"]');
expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('invite');
// switch to nobody
await click('[data-test-members-subscription-option="invite"]');
await click('.ember-power-select-options [data-test-members-subscription-option="none"]');
expect(find('.ember-power-select-options'), 'dropdown closes').to.not.exist;
expect(find('[data-test-members-subscription-option="none"]'), 'none option shown after selected').to.exist;
expect(find('[data-test-iframe="portal-preview"]')).to.not.exist;
expect(find('[data-test-portal-preview-disabled]')).to.exist;
expect(find('[data-test-default-post-access] .ember-basic-dropdown-trigger')).to.have.attribute('aria-disabled', 'true');
await click('[data-test-button="save-settings"]');
expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('none');
// automatically saves when switching back off nobody
await click('[data-test-members-subscription-option="none"]');
await click('.ember-power-select-options [data-test-members-subscription-option="invite"]');
expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('invite');
});
it('can change default post access', async function () {
await visit('/settings/members');
// fixtures match what we expect
expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('public');
expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('[]');
expect(find('[data-test-default-post-access-option="public"]'), 'initial selection is "public"').to.exist;
expect(find('[data-test-default-post-access-tiers]')).to.not.exist;
// open dropdown
await click('[data-test-default-post-access-option="public"]');
// all settings exist in dropdown
expect(find('.ember-power-select-options [data-test-default-post-access-option="public"]'), 'public option').to.exist;
expect(find('.ember-power-select-options [data-test-default-post-access-option="members"]'), 'members-only option').to.exist;
expect(find('.ember-power-select-options [data-test-default-post-access-option="paid"]'), 'paid-only option').to.exist;
expect(find('.ember-power-select-options [data-test-default-post-access-option="tiers"]'), 'specific tiers option').to.exist;
// switch to members only
await click('.ember-power-select-options [data-test-default-post-access-option="members"]');
await click('[data-test-button="save-settings"]');
expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('members');
expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('[]');
expect(find('[data-test-default-post-access-option="members"]'), 'post-members selection is "members"').to.exist;
expect(find('[data-test-default-post-access-tiers]')).to.not.exist;
// can switch to specific tiers
await click('[data-test-default-post-access-option="members"]');
await click('.ember-power-select-options [data-test-default-post-access-option="tiers"]');
// tiers input is shown
expect(find('[data-test-default-post-access-tiers]')).to.exist;
// open tiers dropdown
await click('[data-test-default-post-access-tiers] .ember-basic-dropdown-trigger');
// paid tiers are available in tiers input
expect(find('[data-test-default-post-access-tiers] [data-test-visibility-segment-option="Default Product"]')).to.exist;
// select tier
await click('[data-test-default-post-access-tiers] [data-test-visibility-segment-option="Default Product"]');
// save
await click('[data-test-button="save-settings"]');
expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('tiers');
expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('["2"]');
// switch back to non-tiers option
await click('[data-test-default-post-access-option="tiers"]');
await click('.ember-power-select-options [data-test-default-post-access-option="paid"]');
expect(find('[data-test-default-post-access-tiers]')).to.not.exist;
await click('[data-test-button="save-settings"]');
expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('paid');
expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('["2"]');
});
it('can manage free tier', async function () {
await visit('/settings/members');
await click('[data-test-button="toggle-free-settings"]');
expect(find('[data-test-free-settings-expanded]'), 'expanded free settings').to.exist;
// we aren't viewing the non-labs-flag input
expect(find('[data-test-input="old-free-welcome-page"]')).to.not.exist;
// it can set free signup welcome page
// initial value
expect(find('[data-test-input="free-welcome-page"]')).to.exist;
expect(find('[data-test-input="free-welcome-page"]')).to.have.value('');
// saving
await fillIn('[data-test-input="free-welcome-page"]', 'not a url');
await blur('[data-test-input="free-welcome-page"]');
await click('[data-test-button="save-settings"]');
expect(this.server.db.products.findBy({slug: 'free'}).welcomePageUrl)
.to.equal('/not%20a%20url');
// re-rendering will insert full URL in welcome page input
await visit('/settings');
await visit('/settings/members');
expect(find('[data-test-input="free-welcome-page"]')).to.exist;
expect(find('[data-test-input="free-welcome-page"]'))
.to.have.value('http://localhost:4200/not%20a%20url');
// it can manage free tier description and benefits
// initial free tier details are as expected
expect(find('[data-test-product-card="free"]')).to.exist;
expect(find('[data-test-product-card="free"] [data-test-name]')).to.contain.text('Free');
expect(find('[data-test-product-card="free"] [data-test-description]')).to.contain.text('No description');
expect(find('[data-test-product-card="free"] [data-test-benefits]')).to.contain.text('No benefits');
expect(find('[data-test-product-card="free"] [data-test-free-price]')).to.exist;
// open modal
await click('[data-test-product-card="free"] [data-test-button="edit-product"]');
// initial modal state is as expected
const modal = '[data-test-modal="edit-product"]';
expect(find(modal)).to.exist;
expect(find(`${modal} [data-test-input="product-name"]`)).to.not.exist;
expect(find(`${modal} [data-test-input="product-description"]`)).to.not.exist;
expect(find(`${modal} [data-test-input="free-product-description"]`)).to.exist;
expect(find(`${modal} [data-test-input="free-product-description"]`)).to.have.value('');
expect(find(`${modal} [data-test-formgroup="prices"]`)).to.not.exist;
expect(find(`${modal} [data-test-benefit-item="new"]`)).to.exist;
expect(findAll(`${modal} [data-test-benefit-item]`).length).to.equal(1);
expect(find(`${modal} [data-test-tierpreview-title]`)).to.contain.text('Free Membership Preview');
expect(find(`${modal} [data-test-tierpreview-description]`)).to.contain.text('Free preview of');
expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('Access to all public posts');
expect(find(`${modal} [data-test-tierpreview-price]`).textContent).to.match(/\$\s+0/);
// can change description
await fillIn(`${modal} [data-test-input="free-product-description"]`, 'Test description');
expect(find(`${modal} [data-test-tierpreview-description]`)).to.contain.text('Test description');
// can manage benefits
const newBenefit = `${modal} [data-test-benefit-item="new"]`;
await fillIn(`${newBenefit} [data-test-input="benefit-label"]`, 'First benefit');
await click(`${newBenefit} [data-test-button="add-benefit"]`);
expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('First benefit');
expect(find(`${modal} [data-test-benefit-item="0"]`)).to.exist;
expect(find(`${modal} [data-test-benefit-item="new"]`)).to.exist;
await click(`${newBenefit} [data-test-button="add-benefit"]`);
expect(find(`${newBenefit}`)).to.contain.text('Please enter a benefit');
await fillIn(`${newBenefit} [data-test-input="benefit-label"]`, 'Second benefit');
await click(`${newBenefit} [data-test-button="add-benefit"]`);
expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('Second benefit');
expect(findAll(`${modal} [data-test-tierpreview-benefits] li`).length).to.equal(2);
await click(`${modal} [data-test-benefit-item="0"] [data-test-button="delete-benefit"]`);
expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.not.contain.text('First benefit');
expect(findAll(`${modal} [data-test-tierpreview-benefits] li`).length).to.equal(1);
await click('[data-test-button="save-product"]');
expect(find(`${modal}`)).to.not.exist;
expect(find('[data-test-product-card="free"] [data-test-name]')).to.contain.text('Free');
expect(find('[data-test-product-card="free"] [data-test-description]')).to.contain.text('Test description');
expect(find('[data-test-product-card="free"] [data-test-benefits]')).to.contain.text('Benefits (1)');
expect(find('[data-test-product-card="free"] [data-test-benefits] li:nth-of-type(1)')).to.contain.text('Second benefit');
const freeProduct = this.server.db.products.findBy({slug: 'free'});
expect(freeProduct.description).to.equal('Test description');
expect(freeProduct.welcomePageUrl).to.equal('/not%20a%20url');
expect(freeProduct.productBenefitIds.length).to.equal(1);
const benefits = this.server.db.productBenefits.find(freeProduct.productBenefitIds);
expect(benefits[0].name).to.equal('Second benefit');
});
});

View File

@ -10,7 +10,8 @@ export function enableLabsFlag(server, flag) {
const config = server.schema.configs.first(); const config = server.schema.configs.first();
config.update({enableDeveloperExperiments: true}); config.update({enableDeveloperExperiments: true});
const labsSetting = {}; const existingSetting = server.db.settings.findBy({key: 'labs'}).value;
const labsSetting = existingSetting ? JSON.parse(existingSetting) : {};
labsSetting[flag] = true; labsSetting[flag] = true;
server.db.settings.update({key: 'labs'}, {value: JSON.stringify(labsSetting)}); server.db.settings.update({key: 'labs'}, {value: JSON.stringify(labsSetting)});