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|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-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"}}
@keyPress={{action "clearLabelErrors"}}
@stopEnterKeyDownPropagation={{true}}
@focus-out={{action "updateLabel" this.name}} data-test-input="name" />
@focus-out={{action "updateLabel" this.name}}
data-test-input="benefit-label" />
<GhErrorMessage
@errors={{this.benefitItem.errors}}
@property="name" data-test-error="name" />
@property="name"
data-test-error="benefit-label" />
</GhValidationStatusContainer>
</div>
{{#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>
</button>
{{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>
</button>
{{/if}}

View File

@ -6,12 +6,11 @@
@allowCreation={{false}}
@renderInPlace={{this.renderInPlace}}
@onChange={{this.setSegment}}
@disabled={{@disabled}}
@class="select-members"
@placeholder="Select a tier"
as |option|
>
{{option.name}}
<span data-test-visibility-segment-option={{option.name}}>{{option.name}}</span>
</GhTokenInput>
<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">
<h3 class="gh-product-card-name">
{{this.product.name}}
<h3 class="gh-product-card-name" data-test-name>
{{@product.name}}
</h3>
<p class="gh-product-card-description">
{{#if this.product.description.length}}
{{this.product.description}}
<p class="gh-product-card-description" data-test-description>
{{#if @product.description.length}}
{{@product.description}}
{{else}}
No description added for this tier.
{{/if}}
</p>
</div>
<div class="gh-product-card-block benefits-block">
<h4>Benefits <span class="counter">({{if this.product.benefits.length this.product.benefits.length "0"}})</span></h4>
{{#if this.product.benefits.length}}
<div class="gh-product-card-block benefits-block" data-test-benefits>
<h4>Benefits <span class="counter">({{or @product.benefits.length "0"}})</span></h4>
{{#if @product.benefits.length}}
<ul class="benefits">
{{#each this.product.benefits as |benefit|}}
{{#each @product.benefits as |benefit|}}
<li>{{svg-jar "check"}} {{benefit.name}} </li>
{{/each}}
</ul>
@ -23,10 +23,10 @@
<p class="gh-product-card-description">No benefits added for this tier.</p>
{{/if}}
</div>
{{#if (eq this.product.type "free" )}}
{{#if (eq @product.type "free" )}}
<div class="gh-product-card-block">
<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">
<span class="currency">{{currency-symbol this.productCurrency}}</span>
<span class="amount">0</span>
@ -35,31 +35,31 @@
</div>
</div>
{{/if}}
{{#if (eq this.product.type "paid" )}}
{{#if (eq @product.type "paid" )}}
<div class="gh-product-card-block">
<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">
<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 class="period">Monthly</div>
</div>
<div class="gh-product-card-price">
<div class="gh-product-card-price" data-test-yearly-price>
<div class="flex items-start">
<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 class="period">Yearly</div>
</div>
</div>
</div>
{{/if}}
{{#if (eq this.product.type "paid" )}}
{{#if (eq @product.type "paid" )}}
<div class="gh-product-card-button-container">
<span class="dropdown">
<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"
@title="Tiers Actions"
data-test-button="tiers-actions"
@ -70,19 +70,19 @@
</span>
</GhDropdownButton>
<GhDropdown
@name="tiers-actions-menu-{{this.product.name}}"
@name="tiers-actions-menu-{{@product.name}}"
@tagName="ul"
@classNames="gh-tier-actions-menu dropdown-menu dropdown-triangle-top-right"
>
<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>
</button>
</li>
{{#if this.showArchiveOption}}
<li>
<Settings::Members::ArchiveTier
@product={{this.product}}
@product={{@product}}
@onUnarchive={{@onUnarchive}}
/>
</li>
@ -92,7 +92,7 @@
</div>
{{else}}
<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>
{{svg-jar "pen"}}
</span>

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ export default class SettingsMembersDefaultPostAccess extends Component {
get visibilityTiers() {
const visibilityTiersData = this.settings.get('defaultContentVisibilityTiers');
return visibilityTiersData.map((id) => {
return (visibilityTiersData || []).map((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>
<h4 class="gh-expandable-title">Subscription access</h4>
@ -14,7 +14,7 @@
@dropdownClass="gh-setting-dropdown-list"
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"))}}
<div class="gh-radio-label">
{{option.name}}<br>

View File

@ -50,7 +50,7 @@
<div class="gh-setting-members-portalpreview">
<div class="gh-setting-members-portal-mock {{if (feature "multipleProducts") "mock-enabled"}}">
{{#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>
<h4>Portal disabled</h4>
<p>Change your Subscription Access setting to re-enable Portal</p>
@ -61,7 +61,8 @@
@src={{this.portalPreviewUrl}}
@invisibleUntilLoaded="portal-ready"
@onInserted={{this.portalPreviewInserted}}
@onDestroyed={{this.portalPreviewDestroyed}} />
@onDestroyed={{this.portalPreviewDestroyed}}
data-test-iframe="portal-preview"/>
{{/if}}
</div>
</div>
@ -85,11 +86,11 @@
<h4 class="gh-expandable-title">Free</h4>
<p class="gh-expandable-description">Free member sign up settings</p>
</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 class="gh-expandable-content">
{{#liquid-if this.freeOpen}}
<div class="gh-setting-content-extended">
<div class="gh-setting-content-extended" data-test-free-settings-expanded>
{{#if (feature "multipleProducts")}}
<GhProductCard
@product={{this.freeProduct}}
@ -107,6 +108,7 @@
@setResult={{this.setFreeSignupRedirect}}
@validateUrl={{this.validateFreeSignupRedirect}}
@placeholder={{readonly this.siteUrl}}
data-test-input="old-free-welcome-page"
/>
<GhErrorMessage
@errors={{this.settings.errors}}
@ -124,6 +126,7 @@
@setResult={{this.setWelcomePageURL}}
@validateUrl={{this.validateWelcomePageURL}}
@placeholder={{readonly this.siteUrl}}
data-test-input="free-welcome-page"
/>
<p>Redirect to this URL after signup for a free membership</p>
</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/');
}

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 */
export default [
{
id: 1,
name: 'Test Product',
slug: 'test-product',
monthly_price: {
interval: 'month',
nickname: 'Monthly',
currency: 'usd',
amount: 500
},
yearly_price: {
interval: 'year',
nickname: 'Yearly',
currency: 'usd',
amount: 5000
},
created_at: '2015-11-13T16:01:29.131Z',
created_by: 1,
updated_at: '2015-11-13T16:01:29.131Z',
updated_by: 1
id: '1',
active: true,
benefits: [],
createdAt: '2022-02-04T13:11:40.000Z',
description: null,
monthlyPrice: null,
name: 'Free',
slug: 'free',
type: 'free',
updatedAt: '2022-02-04T13:34:53.000Z',
welcomePageUrl: null,
yearlyPrice: null
},
{
id: '2',
active: true,
benefits: [],
createdAt: '2022-02-04T13:11:40.000Z',
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_by: 1,
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({
// 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 () {
let offer1 = this.server.create('offer', {createdAt: moment.utc().subtract(1, 'day').valueOf()});
this.server.create('offer', {createdAt: moment.utc().subtract(2, 'day').valueOf()});
const product = this.server.create('product');
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');

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();
config.update({enableDeveloperExperiments: true});
const labsSetting = {};
const existingSetting = server.db.settings.findBy({key: 'labs'}).value;
const labsSetting = existingSetting ? JSON.parse(existingSetting) : {};
labsSetting[flag] = true;
server.db.settings.update({key: 'labs'}, {value: JSON.stringify(labsSetting)});