From 7c1f6e14b89c49f8a72b19f0d7938cd80722f780 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 2 Aug 2017 13:32:51 +0400 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Added=20per-post=20code=20?= =?UTF-8?q?injection=20fields=20to=20PSM=20(#811)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove gh-tab* components - The PSM was the only place where the `gh-tabs-manager`, `gh-tab`, and `gh-tab-pane` components were being used. These were very old components and did not work well with newer Ember versions and best practices. - 🔥 remove `gh-tab*` components - 💄 fix indents in `gh-post-settings-menu` template - 🎨 add support for named subviews ready for additional PSM panes - Added per-post code injection fields to PSM - add "Code Injection" pane to the PSM - implement `codeinjectionHead` and `codeinjectionFoot` attributes on `Post` model and save values from PSM - use CodeMirror for the PSM code injection fields --- app/components/gh-cm-editor.js | 6 + app/components/gh-post-settings-menu.js | 43 +- app/components/gh-tab-pane.js | 32 -- app/components/gh-tab.js | 34 -- app/components/gh-tabs-manager.js | 83 ---- app/mixins/editor-base-controller.js | 2 + app/models/post.js | 6 +- app/styles/components/settings-menu.css | 13 + .../components/gh-post-settings-menu.hbs | 394 +++++++++++------- app/validators/post.js | 20 +- mirage/config.js | 2 +- tests/acceptance/editor-test.js | 78 +++- 12 files changed, 389 insertions(+), 324 deletions(-) delete mode 100644 app/components/gh-tab-pane.js delete mode 100644 app/components/gh-tab.js delete mode 100644 app/components/gh-tabs-manager.js diff --git a/app/components/gh-cm-editor.js b/app/components/gh-cm-editor.js index cd1163a0e7..8195dc5aea 100644 --- a/app/components/gh-cm-editor.js +++ b/app/components/gh-cm-editor.js @@ -23,6 +23,12 @@ const CmEditorComponent = Component.extend(InvokeActionMixin, { lazyLoader: injectService(), + didReceiveAttrs() { + if (this.get('value') === null || undefined) { + this.set('value', ''); + } + }, + didInsertElement() { this._super(...arguments); diff --git a/app/components/gh-post-settings-menu.js b/app/components/gh-post-settings-menu.js index bdf10ffd7b..6d11fe379e 100644 --- a/app/components/gh-post-settings-menu.js +++ b/app/components/gh-post-settings-menu.js @@ -29,10 +29,13 @@ export default Component.extend(SettingsMenuMixin, { settings: injectService(), model: null, - slugValue: boundOneWay('model.slug'), + customExcerptScratch: alias('model.customExcerptScratch'), - metaTitleScratch: alias('model.metaTitleScratch'), + codeinjectionFootScratch: alias('model.codeinjectionFootScratch'), + codeinjectionHeadScratch: alias('model.codeinjectionHeadScratch'), metaDescriptionScratch: alias('model.metaDescriptionScratch'), + metaTitleScratch: alias('model.metaTitleScratch'), + slugValue: boundOneWay('model.slug'), _showSettingsMenu: false, _showThrobbers: false, @@ -157,9 +160,11 @@ export default Component.extend(SettingsMenuMixin, { }, actions: { - showSubview() { + showSubview(subview) { this._super(...arguments); + this.set('subview', subview); + // Chrome appears to have an animation bug that cancels the slide // transition unless there's a delay between the animation starting // and the throbbers being removed @@ -170,6 +175,8 @@ export default Component.extend(SettingsMenuMixin, { closeSubview() { this._super(...arguments); + + this.set('subview', null); this.get('showThrobbers').perform(); }, @@ -261,6 +268,36 @@ export default Component.extend(SettingsMenuMixin, { }); }, + setHeaderInjection(code) { + let model = this.get('model'); + let currentCode = model.get('codeinjectionHead'); + + if (code === currentCode) { + return; + } + + model.set('codeinjectionHead', code); + + return model.validate({property: 'codeinjectionHead'}).then(() => { + return model.save(); + }); + }, + + setFooterInjection(code) { + let model = this.get('model'); + let currentCode = model.get('codeinjectionFoot'); + + if (code === currentCode) { + return; + } + + model.set('codeinjectionFoot', code); + + return model.validate({property: 'codeinjectionFoot'}).then(() => { + return model.save(); + }); + }, + setMetaTitle(metaTitle) { // Grab the model and current stored meta title let model = this.get('model'); diff --git a/app/components/gh-tab-pane.js b/app/components/gh-tab-pane.js deleted file mode 100644 index 7c8826ad7f..0000000000 --- a/app/components/gh-tab-pane.js +++ /dev/null @@ -1,32 +0,0 @@ -import Component from 'ember-component'; -import computed, {alias} from 'ember-computed'; - -// See gh-tabs-manager.js for use -export default Component.extend({ - classNameBindings: ['active'], - - tabsManager: computed(function () { - return this.nearestWithProperty('isTabsManager'); - }), - - tab: computed('tabsManager.tabs.[]', 'tabsManager.tabPanes.[]', function () { - let index = this.get('tabsManager.tabPanes').indexOf(this); - let tabs = this.get('tabsManager.tabs'); - - return tabs && tabs.objectAt(index); - }), - - active: alias('tab.active'), - - willRender() { - this._super(...arguments); - // Register with the tabs manager - this.get('tabsManager').registerTabPane(this); - }, - - willDestroyElement() { - this._super(...arguments); - // Deregister with the tabs manager - this.get('tabsManager').unregisterTabPane(this); - } -}); diff --git a/app/components/gh-tab.js b/app/components/gh-tab.js deleted file mode 100644 index 5642debc49..0000000000 --- a/app/components/gh-tab.js +++ /dev/null @@ -1,34 +0,0 @@ -import Component from 'ember-component'; -import computed from 'ember-computed'; - -// See gh-tabs-manager.js for use -export default Component.extend({ - tabsManager: computed(function () { - return this.nearestWithProperty('isTabsManager'); - }), - - active: computed('tabsManager.activeTab', function () { - return this.get('tabsManager.activeTab') === this; - }), - - index: computed('tabsManager.tabs.[]', function () { - return this.get('tabsManager.tabs').indexOf(this); - }), - - // Select on click - click() { - this.get('tabsManager').select(this); - }, - - willRender() { - this._super(...arguments); - // register the tabs with the tab manager - this.get('tabsManager').registerTab(this); - }, - - willDestroyElement() { - this._super(...arguments); - // unregister the tabs with the tab manager - this.get('tabsManager').unregisterTab(this); - } -}); diff --git a/app/components/gh-tabs-manager.js b/app/components/gh-tabs-manager.js deleted file mode 100644 index 660e598621..0000000000 --- a/app/components/gh-tabs-manager.js +++ /dev/null @@ -1,83 +0,0 @@ -import Component from 'ember-component'; - -/** - * Heavily inspired by ic-tabs (https://github.com/instructure/ic-tabs) - * - * Three components work together for smooth tabbing. - * 1. tabs-manager (gh-tabs) - * 2. tab (gh-tab) - * 3. tab-pane (gh-tab-pane) - * - * ## Usage: - * The tabs-manager must wrap all tab and tab-pane components, - * but they can be nested at any level. - - * A tab and its pane are tied together via their order. - * So, the second tab within a tab manager will activate - * the second pane within that manager. - - * ```hbs - * {{#gh-tabs-manager}} - * {{#gh-tab}} - * First tab - * {{/gh-tab}} - * {{#gh-tab}} - * Second tab - * {{/gh-tab}} - * - * .... - * {{#gh-tab-pane}} - * First pane - * {{/gh-tab-pane}} - * {{#gh-tab-pane}} - * Second pane - * {{/gh-tab-pane}} - * {{/gh-tabs-manager}} - * ``` - * ## Options: - * - * the tabs-manager will send a "selected" action whenever one of its - * tabs is clicked. - * ```hbs - * {{#gh-tabs-manager selected="myAction"}} - * .... - * {{/gh-tabs-manager}} - * ``` - * - * ## Styling: - * Both tab and tab-pane elements have an "active" - * class applied when they are active. - * - */ -export default Component.extend({ - activeTab: null, - tabs: [], - tabPanes: [], - - // Used by children to find this tabsManager - isTabsManager: true, - - // Called when a gh-tab is clicked. - select(tab) { - this.set('activeTab', tab); - this.sendAction('selected'); - }, - - // Register tabs and their panes to allow for - // interaction between components. - registerTab(tab) { - this.get('tabs').addObject(tab); - }, - - unregisterTab(tab) { - this.get('tabs').removeObject(tab); - }, - - registerTabPane(tabPane) { - this.get('tabPanes').addObject(tabPane); - }, - - unregisterTabPane(tabPane) { - this.get('tabPanes').removeObject(tabPane); - } -}); diff --git a/app/mixins/editor-base-controller.js b/app/mixins/editor-base-controller.js index 8e6699bd94..ccfcc67dbf 100644 --- a/app/mixins/editor-base-controller.js +++ b/app/mixins/editor-base-controller.js @@ -159,6 +159,8 @@ export default Mixin.create({ this.set('model.title', this.get('model.titleScratch')); this.set('model.customExcerpt', this.get('model.customExcerptScratch')); + this.set('model.footerInjection', this.get('model.footerExcerptScratch')); + this.set('model.headerInjection', this.get('model.headerExcerptScratch')); this.set('model.metaTitle', this.get('model.metaTitleScratch')); this.set('model.metaDescription', this.get('model.metaDescriptionScratch')); diff --git a/app/models/post.js b/app/models/post.js index 5ac0b1027c..0625bdef9c 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -80,6 +80,8 @@ export default Model.extend(Comparable, ValidationEngine, { customExcerpt: attr(), featured: attr('boolean', {defaultValue: false}), featureImage: attr('string'), + codeinjectionFoot: attr('string', {defaultValue: ''}), + codeinjectionHead: attr('string', {defaultValue: ''}), html: attr('string'), locale: attr('string'), metaDescription: attr('string'), @@ -116,8 +118,10 @@ export default Model.extend(Comparable, ValidationEngine, { publishedAtBlogTime: '', customExcerptScratch: boundOneWay('customExcerpt'), - metaTitleScratch: boundOneWay('metaTitle'), + codeinjectionFootScratch: boundOneWay('codeinjectionFoot'), + codeinjectionHeadScratch: boundOneWay('codeinjectionHead'), metaDescriptionScratch: boundOneWay('metaDescription'), + metaTitleScratch: boundOneWay('metaTitle'), isPublished: equal('status', 'published'), isDraft: equal('status', 'draft'), diff --git a/app/styles/components/settings-menu.css b/app/styles/components/settings-menu.css index b28333b2bb..db4515cc9c 100644 --- a/app/styles/components/settings-menu.css +++ b/app/styles/components/settings-menu.css @@ -111,6 +111,10 @@ padding: 0 24px 24px; } +.settings-menu-content label code { + font-weight: normal; +} + .settings-menu-content .gh-image-uploader { margin: 0 0 1.6rem 0; } @@ -188,6 +192,15 @@ line-height: 1.35em; } +.settings-menu-content .CodeMirror { + height: 170px; + min-height: 170px; + padding: 0; +} + +.settings-menu-content .CodeMirror-scroll { + min-height: 170px; +} /* Background /* ---------------------------------------------------------- */ diff --git a/app/templates/components/gh-post-settings-menu.hbs b/app/templates/components/gh-post-settings-menu.hbs index 9f97549be9..246a4e1e9b 100644 --- a/app/templates/components/gh-post-settings-menu.hbs +++ b/app/templates/components/gh-post-settings-menu.hbs @@ -1,177 +1,249 @@ -{{#gh-tabs-manager selected=(action "showSubview") id="entry-controls" class="settings-menu-container"}} -
-
-
-

Post Settings

- -
-
- {{gh-image-uploader-with-preview - image=model.featureImage - text="Add post image" - update=(action "setCoverImage") - remove=(action "clearCoverImage") - }} -
-
- - {{!-- new posts don't have a preview link --}} - {{#unless model.isNew}} - {{#if model.isPublished}} - - View post {{inline-svg "external"}} - +
+
+
+
+

Post Settings

+ +
+
+ {{gh-image-uploader-with-preview + image=model.featureImage + text="Add post image" + update=(action "setCoverImage") + remove=(action "clearCoverImage") + }} + +
+ + {{!-- new posts don't have a preview link --}} + {{#unless model.isNew}} + {{#if model.isPublished}} + + View post {{inline-svg "external"}} + + {{else}} + + Preview {{inline-svg "external"}} + + {{/if}} + {{/unless}} + + + {{gh-url-preview slug=slugValue tagName="p" classNames="description"}} +
+ +
+ {{#if (or model.isDraft model.isPublished model.pastScheduledTime)}} + {{else}} - - Preview {{inline-svg "external"}} - + +

Use the publish menu to re-schedule

{{/if}} - {{/unless}} - - - {{gh-url-preview slug=slugValue tagName="p" classNames="description"}} -
-
- {{#if (or model.isDraft model.isPublished model.pastScheduledTime)}} - - {{else}} - -

Use the publish menu to re-schedule

- {{/if}} - {{gh-date-time-picker - date=model.publishedAtBlogDate - time=model.publishedAtBlogTime - setDate=(action "setPublishedAtBlogDate") - setTime=(action "setPublishedAtBlogTime") - errors=model.errors - dateErrorProperty="publishedAtBlogDate" - timeErrorProperty="publishedAtBlogTime" - maxDate='now' - disabled=model.isScheduled - static=true - }} -
+
+ + {{gh-selectize + id="tag-input" + multiple=true + selection=model.tags + content=availableTags + optionValuePath="content.uuid" + optionLabelPath="content.name" + openOnFocus=false + create-item="addTag" + remove-item="removeTag" + plugins="remove_button, drag_drop"}} +
-
- - {{gh-selectize - id="tag-input" - multiple=true - selection=model.tags - content=availableTags - optionValuePath="content.uuid" - optionLabelPath="content.name" - openOnFocus=false - create-item="addTag" - remove-item="removeTag" - plugins="remove_button, drag_drop"}} -
+ {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="customExcerpt"}} + + {{gh-textarea customExcerptScratch + class="post-setting-custom-excerpt" + id="custom-excerpt" + name="post-setting-custom-excerpt" + focusOut=(action "setCustomExcerpt" customExcerptScratch) + stopEnterKeyDownPropagation="true" + update=(action (mut customExcerptScratch)) + data-test-field="custom-excerpt"}} + {{gh-error-message errors=model.errors property="customExcerpt" data-test-error="custom-excerpt"}} + {{/gh-form-group}} - {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="customExcerpt"}} - - {{gh-textarea customExcerptScratch class="post-setting-custom-excerpt" id="custom-excerpt" name="post-setting-custom-excerpt" focusOut=(action "setCustomExcerpt" customExcerptScratch) stopEnterKeyDownPropagation="true" update=(action (mut customExcerptScratch)) data-test-field="custom-excerpt"}} - {{gh-error-message errors=model.errors property="customExcerpt" data-test-error="custom-excerpt"}} - {{/gh-form-group}} - - {{#unless session.user.isAuthor}} -
- - - {{inline-svg "user-circle"}} - - {{one-way-select - selectedAuthor - id="author-list" - name="post-setting-author" - options=authors - optionValuePath="id" - optionLabelPath="name" - update=(action "changeAuthor") - }} - {{inline-svg "arrow-down-small"}} + {{#unless session.user.isAuthor}} +
+ + + {{inline-svg "user-circle"}} + + {{one-way-select + selectedAuthor + id="author-list" + name="post-setting-author" + options=authors + optionValuePath="id" + optionLabelPath="name" + update=(action "changeAuthor") + }} + {{inline-svg "arrow-down-small"}} + - -
- {{/unless}} - - - -
- - - -
+
+ {{/unless}} - {{#unless model.isNew}} - - {{/unless}} - - -
{{! .settings-menu-content }} -
{{! .post-settings-menu }} - -
- {{#gh-tab-pane}} - {{#if isViewingSubview}} -
- -

Meta Data

-
{{!flexbox space-between}}
-
+ -
-
- {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="metaTitle"}} - - {{gh-input metaTitleScratch class="post-setting-meta-title" id="meta-title" name="post-setting-meta-title" focusOut=(action "setMetaTitle" metaTitleScratch) stopEnterKeyDownPropagation="true" update=(action (mut metaTitleScratch))}} -

Recommended: 70 characters. You’ve used {{gh-count-down-characters metaTitleScratch 70}}

- {{gh-error-message errors=model.errors property="metaTitle"}} - {{/gh-form-group}} - - {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="metaDescription"}} - - {{gh-textarea metaDescriptionScratch class="post-setting-meta-description" id="meta-description" name="post-setting-meta-description" focusOut=(action "setMetaDescription" metaDescriptionScratch) stopEnterKeyDownPropagation="true" update=(action (mut metaDescriptionScratch))}} -

Recommended: 156 characters. You’ve used {{gh-count-down-characters metaDescriptionScratch 156}}

- {{gh-error-message errors=model.errors property="metaDescription"}} - {{/gh-form-group}} - -
- -
-
{{seoTitle}}
- -
{{seoDescription}}
+
+ + +
+ + {{#unless model.isNew}} + + {{/unless}} + + +
{{! .settings-menu-content }} +
{{! .post-settings-menu }} + +
+
+ {{#if isViewingSubview}} + {{#if (eq subview "meta-data")}} +
+ +

Meta Data

+
+
+ +
+
+ {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="metaTitle"}} + + {{gh-input metaTitleScratch + class="post-setting-meta-title" + id="meta-title" + name="post-setting-meta-title" + focusOut=(action "setMetaTitle" metaTitleScratch) + stopEnterKeyDownPropagation="true" + update=(action (mut metaTitleScratch))}} +

Recommended: 70 characters. You’ve used {{gh-count-down-characters metaTitleScratch 70}}

+ {{gh-error-message errors=model.errors property="metaTitle"}} + {{/gh-form-group}} + + {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="metaDescription"}} + + {{gh-textarea metaDescriptionScratch + class="post-setting-meta-description" + id="meta-description" + name="post-setting-meta-description" + focusOut=(action "setMetaDescription" metaDescriptionScratch) + stopEnterKeyDownPropagation="true" + update=(action (mut metaDescriptionScratch))}} +

Recommended: 156 characters. You’ve used {{gh-count-down-characters metaDescriptionScratch 156}}

+ {{gh-error-message errors=model.errors property="metaDescription"}} + {{/gh-form-group}} + +
+ +
+
{{seoTitle}}
+ +
{{seoDescription}}
+
+
+
+
+ {{/if}} + + {{#if (eq subview "codeinjection")}} +
+ +

Code Injection

+
+
+ +
+
+ {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="codeinjectionHead"}} + + {{gh-cm-editor codeinjectionHeadScratch + id="post-setting-codeinjection-head" + class="post-setting-codeinjection" + name="post-setting-codeinjection-head" + focusOut=(action "setHeaderInjection" codeinjectionHeadScratch) + stopEnterKeyDownPropagation="true" + update=(action (mut codeinjectionHeadScratch)) + data-test-field="codeinjection-head"}} + {{gh-error-message errors=model.errors property="codeinjectionHead" data-test-error="codeinjection-head"}} + {{/gh-form-group}} + + {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="codeinjectionFoot"}} + + {{gh-cm-editor codeinjectionFootScratch + id="post-setting-codeinjection-foot" + class="post-setting-codeinjection" + name="post-setting-codeinjection-foot" + focusOut=(action "setFooterInjection" codeinjectionFootScratch) + stopEnterKeyDownPropagation="true" + update=(action (mut codeinjectionFootScratch)) + data-test-field="codeinjection-foot"}} + {{gh-error-message errors=model.errors property="codeinjectionFoot" data-test-error="codeinjection-foot"}} + {{/gh-form-group}} +
+
+ {{/if}} + {{/if}}
- -
{{! .settings-menu-content }} - {{/if}} - {{/gh-tab-pane}} +
-{{/gh-tabs-manager}} {{!-- _showThrobbers is on a timer so that throbbers don't get positioned until diff --git a/app/validators/post.js b/app/validators/post.js index b947aee19c..0ad679ed13 100644 --- a/app/validators/post.js +++ b/app/validators/post.js @@ -3,7 +3,7 @@ import moment from 'moment'; import {isEmpty, isPresent} from 'ember-utils'; export default BaseValidator.create({ - properties: ['title', 'customExcerpt', 'metaTitle', 'metaDescription', 'publishedAtBlogTime', 'publishedAtBlogDate'], + properties: ['title', 'customExcerpt', 'codeinjectionHead', 'codeinjectionFoot', 'metaTitle', 'metaDescription', 'publishedAtBlogTime', 'publishedAtBlogDate'], title(model) { let title = model.get('title'); @@ -28,6 +28,24 @@ export default BaseValidator.create({ } }, + codeinjectionFoot(model) { + let codeinjectionFoot = model.get('codeinjectionFoot'); + + if (!validator.isLength(codeinjectionFoot, 0, 65535)) { + model.get('errors').add('codeinjectionFoot', 'Footer code cannot be longer than 65535 characters.'); + this.invalidate(); + } + }, + + codeinjectionHead(model) { + let codeinjectionHead = model.get('codeinjectionHead'); + + if (!validator.isLength(codeinjectionHead, 0, 65535)) { + model.get('errors').add('codeinjectionHead', 'Header code cannot be longer than 65535 characters.'); + this.invalidate(); + } + }, + metaTitle(model) { let metaTitle = model.get('metaTitle'); diff --git a/mirage/config.js b/mirage/config.js index 6af9e6db83..14fa48e234 100644 --- a/mirage/config.js +++ b/mirage/config.js @@ -37,7 +37,7 @@ export function testConfig() { // this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server this.namespace = '/ghost/api/v0.1'; // make this `api`, for example, if your API is namespaced // this.timing = 400; // delay for each request, automatically set to 0 during testing - // this.logging = true; + this.logging = true; mockAuthentication(this); mockConfiguration(this); diff --git a/tests/acceptance/editor-test.js b/tests/acceptance/editor-test.js index d4bc014269..f679f430f0 100644 --- a/tests/acceptance/editor-test.js +++ b/tests/acceptance/editor-test.js @@ -556,15 +556,7 @@ describe('Acceptance: Editor', function() { // TODO: implement tests for other fields - // changing custom excerpt auto-saves await click(testSelector('psm-trigger')); - await fillIn(testSelector('field', 'custom-excerpt'), 'Testing excerpt'); - await triggerEvent(testSelector('field', 'custom-excerpt'), 'blur'); - - expect( - server.db.posts[0].custom_excerpt, - 'saved excerpt' - ).to.equal('Testing excerpt'); // excerpt has validation await fillIn(testSelector('field', 'custom-excerpt'), Array(302).join('a')); @@ -578,7 +570,77 @@ describe('Acceptance: Editor', function() { expect( server.db.posts[0].custom_excerpt, 'saved excerpt after validation error' + ).to.be.blank; + + // changing custom excerpt auto-saves + await fillIn(testSelector('field', 'custom-excerpt'), 'Testing excerpt'); + await triggerEvent(testSelector('field', 'custom-excerpt'), 'blur'); + + expect( + server.db.posts[0].custom_excerpt, + 'saved excerpt' ).to.equal('Testing excerpt'); + + // ------- + + // open code injection subview + await click(testSelector('button', 'codeinjection')); + + // header injection has validation + let headerCM = find(`${testSelector('field', 'codeinjection-head')} .CodeMirror`)[0].CodeMirror; + await headerCM.setValue(Array(65540).join('a')); + await triggerEvent(headerCM.getInputField(), 'blur'); + + expect( + find(testSelector('error', 'codeinjection-head')).text().trim(), + 'header injection too long error' + ).to.match(/cannot be longer than 65535/); + + expect( + server.db.posts[0].codeinjection_head, + 'saved header injection after validation error' + ).to.be.blank; + + // changing header injection auto-saves + await headerCM.setValue(''); + await triggerEvent(headerCM.getInputField(), 'blur'); + + expect( + server.db.posts[0].codeinjection_head, + 'saved header injection' + ).to.equal(''); + + // footer injection has validation + let footerCM = find(`${testSelector('field', 'codeinjection-foot')} .CodeMirror`)[0].CodeMirror; + await footerCM.setValue(Array(65540).join('a')); + await triggerEvent(footerCM.getInputField(), 'blur'); + + expect( + find(testSelector('error', 'codeinjection-foot')).text().trim(), + 'footer injection too long error' + ).to.match(/cannot be longer than 65535/); + + expect( + server.db.posts[0].codeinjection_foot, + 'saved footer injection after validation error' + ).to.be.blank; + + // changing footer injection auto-saves + await footerCM.setValue(''); + await triggerEvent(footerCM.getInputField(), 'blur'); + + expect( + server.db.posts[0].codeinjection_foot, + 'saved footer injection' + ).to.equal(''); + + // closing subview switches back to main PSM view + await click(testSelector('button', 'close-psm-subview')); + + expect( + find(testSelector('field', 'codeinjection-head')).length, + 'header injection not present after closing subview' + ).to.equal(0); }); }); });