From 26965fa08f8a5e834b01f743109c835a62724706 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 25 Mar 2016 14:41:15 +0000 Subject: [PATCH 1/4] Added a markdown editor --- app/Http/Controllers/PageController.php | 5 +- app/Page.php | 2 +- app/PageRevision.php | 2 +- config/app.php | 2 + ...2016_03_25_123157_add_markdown_support.php | 39 ++++++++++++ package.json | 1 + .../fonts/roboto-mono-v4-latin-regular.woff | Bin 0 -> 19592 bytes .../fonts/roboto-mono-v4-latin-regular.woff2 | Bin 0 -> 16108 bytes readme.md | 1 + resources/assets/js/controllers.js | 45 +++++++++----- resources/assets/js/directives.js | 32 ++++++++++ resources/assets/sass/_fonts.scss | 11 ++++ resources/assets/sass/_forms.scss | 56 ++++++++++++++++++ resources/assets/sass/_text.scss | 6 ++ resources/views/pages/form.blade.php | 31 ++++++++-- 15 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 database/migrations/2016_03_25_123157_add_markdown_support.php create mode 100644 public/fonts/roboto-mono-v4-latin-regular.woff create mode 100644 public/fonts/roboto-mono-v4-latin-regular.woff2 diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index e2b10d3d3..c3d8e396c 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -164,6 +164,7 @@ class PageController extends Controller $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); $page->name = $draft->name; $page->html = $draft->html; + $page->markdown = $draft->markdown; $page->isDraft = true; $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft); } @@ -204,9 +205,9 @@ class PageController extends Controller $page = $this->pageRepo->getById($pageId, true); $this->checkOwnablePermission('page-update', $page); if ($page->draft) { - $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html'])); + $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown'])); } else { - $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); + $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown'])); } $updateTime = $draft->updated_at->format('H:i'); return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]); diff --git a/app/Page.php b/app/Page.php index 84e37d519..d2a303f61 100644 --- a/app/Page.php +++ b/app/Page.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; class Page extends Entity { - protected $fillable = ['name', 'html', 'priority']; + protected $fillable = ['name', 'html', 'priority', 'markdown']; protected $simpleAttributes = ['name', 'id', 'slug']; diff --git a/app/PageRevision.php b/app/PageRevision.php index f1b4bc587..c258913ff 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model; class PageRevision extends Model { - protected $fillable = ['name', 'html', 'text']; + protected $fillable = ['name', 'html', 'text', 'markdown']; /** * Get the user that created the page revision diff --git a/config/app.php b/config/app.php index 650ad1d07..d305af3c0 100644 --- a/config/app.php +++ b/config/app.php @@ -5,6 +5,8 @@ return [ 'env' => env('APP_ENV', 'production'), + 'editor' => env('APP_EDITOR', 'html'), + /* |-------------------------------------------------------------------------- | Application Debug Mode diff --git a/database/migrations/2016_03_25_123157_add_markdown_support.php b/database/migrations/2016_03_25_123157_add_markdown_support.php new file mode 100644 index 000000000..45efe5a09 --- /dev/null +++ b/database/migrations/2016_03_25_123157_add_markdown_support.php @@ -0,0 +1,39 @@ +longText('markdown'); + }); + + Schema::table('page_revisions', function (Blueprint $table) { + $table->longText('markdown'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('pages', function (Blueprint $table) { + $table->dropColumn('markdown'); + }); + + Schema::table('page_revisions', function (Blueprint $table) { + $table->dropColumn('markdown'); + }); + } +} diff --git a/package.json b/package.json index a1fb06b1c..7d1aa1a6a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "bootstrap-sass": "^3.0.0", "dropzone": "^4.0.1", "laravel-elixir": "^3.4.0", + "marked": "^0.3.5", "zeroclipboard": "^2.2.0" } } diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff b/public/fonts/roboto-mono-v4-latin-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..8cb9e6fd8840b38960ce4348be2652ce5f41bcef GIT binary patch literal 19592 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 literal 0 HcmV?d00001 diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff2 b/public/fonts/roboto-mono-v4-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1f6598111497129d63a60d571c0dfa83296f619b GIT binary patch literal 16108 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 yy# { var newTitle = $('#name').val(); - var newHtml = $scope.editorHtml; + var newHtml = $scope.editContent; if (newTitle !== currentContent.title || newHtml !== currentContent.html) { currentContent.html = newHtml; currentContent.title = newTitle; - saveDraft(newTitle, newHtml); + saveDraft(); } + }, 1000 * autosaveFrequency); } @@ -272,20 +284,24 @@ module.exports = function (ngApp, events) { * @param title * @param html */ - function saveDraft(title, html) { - $http.put('/ajax/page/' + pageId + '/save-draft', { - name: title, - html: html - }).then((responseData) => { + function saveDraft() { + var data = { + name: $('#name').val(), + html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent + }; + + if (isMarkdown) data.markdown = $scope.editContent; + + console.log(data.markdown); + + $http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => { $scope.draftText = responseData.data.message; if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; }); } $scope.forceDraftSave = function() { - var newTitle = $('#name').val(); - var newHtml = $scope.editorHtml; - saveDraft(newTitle, newHtml); + saveDraft(); }; /** @@ -298,6 +314,7 @@ module.exports = function (ngApp, events) { $scope.draftText = 'Editing Page'; $scope.isUpdateDraft = false; $scope.$broadcast('html-update', responseData.data.html); + $scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html); $('#name').val(responseData.data.name); $timeout(() => { startAutoSave(); diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 71b35fb42..316e5dcb4 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,5 +1,6 @@ "use strict"; var DropZone = require('dropzone'); +var markdown = require( "marked" ); var toggleSwitchTemplate = require('./components/toggle-switch.html'); var imagePickerTemplate = require('./components/image-picker.html'); @@ -202,5 +203,36 @@ module.exports = function (ngApp, events) { } }]) + ngApp.directive('markdownEditor', ['$timeout', function($timeout) { + return { + restrict: 'A', + scope: { + mdModel: '=', + mdChange: '=' + }, + link: function (scope, element, attrs) { + + // Set initial model content + var content = element.val(); + scope.mdModel = content; + scope.mdChange(markdown(content)); + + element.on('change input', (e) => { + content = element.val(); + $timeout(() => { + scope.mdModel = content; + scope.mdChange(markdown(content)); + }); + }); + + scope.$on('markdown-update', (event, value) => { + element.val(value); + scope.mdModel= value; + scope.mdChange(markdown(value)); + }); + + } + } + }]) }; \ No newline at end of file diff --git a/resources/assets/sass/_fonts.scss b/resources/assets/sass/_fonts.scss index 0dc8c95b2..8cf677779 100644 --- a/resources/assets/sass/_fonts.scss +++ b/resources/assets/sass/_fonts.scss @@ -93,4 +93,15 @@ url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg'); font-weight: normal; font-style: normal; +} + +/* roboto-mono-regular - latin */ +// https://google-webfonts-helper.herokuapp.com +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + src: local('Roboto Mono'), local('RobotoMono-Regular'), + url('/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } \ No newline at end of file diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 037dad94a..5351f06e7 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -26,6 +26,58 @@ display: none; } +#markdown-editor { + position: relative; + z-index: 5; + textarea { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + padding: $-xs $-m; + color: #444; + border-radius: 0; + max-height: 100%; + flex: 1; + border: 0; + &:focus { + outline: 0; + } + } + .markdown-display, .markdown-editor-wrap { + flex: 1; + padding-top: 28px; + position: relative; + border: 1px solid #DDD; + &:before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + padding: $-xs $-m; + font-family: 'Roboto Mono'; + font-size: 11px; + line-height: 1; + border-bottom: 1px solid #DDD; + background-color: #EEE; + } + } + .markdown-editor-wrap { + display: flex; + &:before { + content: 'Editor'; + } + } + .markdown-display { + padding: 0 $-m; + padding-top: 28px; + margin-left: -1px; + &:before { + content: 'Preview'; + } + } +} + label { display: block; line-height: 1.4em; @@ -160,6 +212,10 @@ input:checked + .toggle-switch { width: 100%; } +div[editor-type="markdown"] .title-input.page-title input[type="text"] { + max-width: 100%; +} + .search-box { max-width: 100%; position: relative; diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 721eeb238..1a55cf868 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -157,6 +157,12 @@ span.code { @extend .code-base; padding: 1px $-xs; } + +pre code { + background-color: transparent; + border: 0; + font-size: 1em; +} /* * Text colors */ diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index d8dc19ec2..2118d23b2 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,5 +1,5 @@ -
+
{{ csrf_field() }}
@@ -42,10 +42,31 @@
- - @if($errors->has('html')) -
{{ $errors->first('html') }}
+ @if(config('app.editor') === 'html') + + @if($errors->has('html')) +
{{ $errors->first('html') }}
+ @endif + @endif + + @if(config('app.editor') === 'markdown') +
+ +
+ +
+ +
+
+ + + + @if($errors->has('markdown')) +
{{ $errors->first('markdown') }}
+ @endif + @endif
\ No newline at end of file From efb49019d484f2455897d2249e16ccd0b2ad5e88 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 29 Mar 2016 18:25:54 +0100 Subject: [PATCH 2/4] Integrated the markdown editor with the image manager --- resources/assets/js/directives.js | 50 ++++++++++++++++++++++++++-- resources/assets/sass/_forms.scss | 44 ++++++++++++------------ resources/views/pages/form.blade.php | 18 ++++++++-- 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 316e5dcb4..de87950dc 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,6 +1,6 @@ "use strict"; var DropZone = require('dropzone'); -var markdown = require( "marked" ); +var markdown = require('marked'); var toggleSwitchTemplate = require('./components/toggle-switch.html'); var imagePickerTemplate = require('./components/image-picker.html'); @@ -201,9 +201,9 @@ module.exports = function (ngApp, events) { tinymce.init(scope.tinymce); } } - }]) + }]); - ngApp.directive('markdownEditor', ['$timeout', function($timeout) { + ngApp.directive('markdownInput', ['$timeout', function($timeout) { return { restrict: 'A', scope: { @@ -231,6 +231,50 @@ module.exports = function (ngApp, events) { scope.mdChange(markdown(value)); }); + } + } + }]); + + ngApp.directive('markdownEditor', ['$timeout', function($timeout) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + + // Elements + var input = element.find('textarea[markdown-input]'); + var insertImage = element.find('button[data-action="insertImage"]'); + + var currentCaretPos = 0; + + input.blur((event) => { + currentCaretPos = input[0].selectionStart; + }); + + // Insert image shortcut + input.keydown((event) => { + if (event.which === 73 && event.ctrlKey && event.shiftKey) { + event.preventDefault(); + var caretPos = input[0].selectionStart; + var currentContent = input.val(); + var mdImageText = "![](http://)"; + input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); + input.focus(); + input[0].selectionStart = caretPos + ("![](".length); + input[0].selectionEnd = caretPos + ('![](http://'.length); + } + }); + + // Insert image from image manager + insertImage.click((event) => { + window.ImageManager.showExternal((image) => { + var caretPos = currentCaretPos; + var currentContent = input.val(); + var mdImageText = "![" + image.name + "](" + image.url + ")"; + input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); + input.change(); + }); + }); + } } }]) diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 5351f06e7..4da0c39ad 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -39,44 +39,42 @@ max-height: 100%; flex: 1; border: 0; + width: 100%; &:focus { outline: 0; } } .markdown-display, .markdown-editor-wrap { flex: 1; - padding-top: 28px; position: relative; - border: 1px solid #DDD; - &:before { - display: block; - position: absolute; - top: 0; - left: 0; - width: 100%; - padding: $-xs $-m; - font-family: 'Roboto Mono'; - font-size: 11px; - line-height: 1; - border-bottom: 1px solid #DDD; - background-color: #EEE; - } } .markdown-editor-wrap { display: flex; - &:before { - content: 'Editor'; - } + flex-direction: column; + border: 1px solid #DDD; } .markdown-display { - padding: 0 $-m; - padding-top: 28px; + padding: 0 $-m 0; margin-left: -1px; - &:before { - content: 'Preview'; - } + overflow-y: scroll; } } +.editor-toolbar { + width: 100%; + padding: $-xs $-m; + font-family: 'Roboto Mono'; + font-size: 11px; + line-height: 1.6; + border-bottom: 1px solid #DDD; + background-color: #EEE; + flex: none; + &:after { + content: ''; + display: block; + clear: both; + } +} + label { display: block; diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index 2118d23b2..6b16cb870 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -51,14 +51,26 @@ @endif @if(config('app.editor') === 'markdown') -
+
-
-
+
+
+
Preview
+
+
+
+
From e1994ef2cf6552516888422063cf859586f0de14 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 29 Mar 2016 19:26:13 +0100 Subject: [PATCH 3/4] Added editor control in admin settings & Fixed some markdown editor bugs Also updated the setting system with a more sane approach to handling default values. (Now done via the setting-defaults config file) --- app/Repos/PageRepo.php | 4 +++ app/Services/SettingService.php | 13 ++++++++- config/setting-defaults.php | 10 +++++++ ...2016_03_25_123157_add_markdown_support.php | 4 +-- resources/assets/js/controllers.js | 6 ++-- resources/views/pages/form.blade.php | 6 ++-- resources/views/settings/index.blade.php | 28 ++++++++++++------- 7 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 config/setting-defaults.php diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 4c3512fa7..9a7502754 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -312,6 +312,7 @@ class PageRepo extends EntityRepo $page->fill($input); $page->html = $this->formatHtml($input['html']); $page->text = strip_tags($page->html); + if (setting('app-editor') !== 'markdown') $page->markdown = ''; $page->updated_by = $userId; $page->save(); @@ -348,6 +349,7 @@ class PageRepo extends EntityRepo public function saveRevision(Page $page) { $revision = $this->pageRevision->fill($page->toArray()); + if (setting('app-editor') !== 'markdown') $revision->markdown = ''; $revision->page_id = $page->id; $revision->slug = $page->slug; $revision->book_slug = $page->book->slug; @@ -386,6 +388,8 @@ class PageRepo extends EntityRepo } $draft->fill($data); + if (setting('app-editor') !== 'markdown') $draft->markdown = ''; + $draft->save(); return $draft; } diff --git a/app/Services/SettingService.php b/app/Services/SettingService.php index bcc7eae31..bf5fa918e 100644 --- a/app/Services/SettingService.php +++ b/app/Services/SettingService.php @@ -44,28 +44,39 @@ class SettingService /** * Gets a setting value from the cache or database. + * Looks at the system defaults if not cached or in database. * @param $key * @param $default * @return mixed */ protected function getValueFromStore($key, $default) { + // Check for an overriding value $overrideValue = $this->getOverrideValue($key); if ($overrideValue !== null) return $overrideValue; + // Check the cache $cacheKey = $this->cachePrefix . $key; if ($this->cache->has($cacheKey)) { return $this->cache->get($cacheKey); } + // Check the database $settingObject = $this->getSettingObjectByKey($key); - if ($settingObject !== null) { $value = $settingObject->value; $this->cache->forever($cacheKey, $value); return $value; } + // Check the defaults set in the app config. + $configPrefix = 'setting-defaults.' . $key; + if (config()->has($configPrefix)) { + $value = config($configPrefix); + $this->cache->forever($cacheKey, $value); + return $value; + } + return $default; } diff --git a/config/setting-defaults.php b/config/setting-defaults.php new file mode 100644 index 000000000..17bae1848 --- /dev/null +++ b/config/setting-defaults.php @@ -0,0 +1,10 @@ + 'wysiwyg' + +]; \ No newline at end of file diff --git a/database/migrations/2016_03_25_123157_add_markdown_support.php b/database/migrations/2016_03_25_123157_add_markdown_support.php index 45efe5a09..2daa32cfb 100644 --- a/database/migrations/2016_03_25_123157_add_markdown_support.php +++ b/database/migrations/2016_03_25_123157_add_markdown_support.php @@ -13,11 +13,11 @@ class AddMarkdownSupport extends Migration public function up() { Schema::table('pages', function (Blueprint $table) { - $table->longText('markdown'); + $table->longText('markdown')->default(''); }); Schema::table('page_revisions', function (Blueprint $table) { - $table->longText('markdown'); + $table->longText('markdown')->default(''); }); } diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 09187c0c2..dbd2e1ae6 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -258,6 +258,10 @@ module.exports = function (ngApp, events) { } } + if (!isMarkdown) { + $scope.editorChange = function() {}; + } + /** * Start the AutoSave loop, Checks for content change * before performing the costly AJAX request. @@ -292,8 +296,6 @@ module.exports = function (ngApp, events) { if (isMarkdown) data.markdown = $scope.editContent; - console.log(data.markdown); - $http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => { $scope.draftText = responseData.data.message; if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index 6b16cb870..fe3d28cbc 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,5 +1,5 @@ -
+
{{ csrf_field() }}
@@ -42,7 +42,7 @@
- @if(config('app.editor') === 'html') + @if(setting('app-editor') === 'wysiwyg') @if($errors->has('html')) @@ -50,7 +50,7 @@ @endif @endif - @if(config('app.editor') === 'markdown') + @if(setting('app-editor') === 'markdown')
diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index f94623256..eb580bb8b 100644 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -17,29 +17,37 @@
- +
- +

For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.

- + +
+
+ +

Select which editor will be used by all users to edit pages.

+

This image should be 43px in height.
Large images will be scaled down.

- +

This should be a hex value.
Leave empty to reset to the default color.

- - + +
@@ -53,14 +61,14 @@
- +
+
From dc2978824ee9a783402e4c28934965f5dcffeb0a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 29 Mar 2016 20:13:23 +0100 Subject: [PATCH 4/4] Added basic system tests for markdown editor, Added extra test helpers Added test helpers for checking if an element exists / does not exist on a page. Also fixed markdown editor bugs found while creating tests. --- resources/assets/sass/_forms.scss | 3 ++ resources/views/pages/form.blade.php | 4 ++- tests/Entity/MarkdownTest.php | 51 ++++++++++++++++++++++++++++ tests/TestCase.php | 24 +++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/Entity/MarkdownTest.php diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 4da0c39ad..4a505c5f8 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -57,6 +57,9 @@ padding: 0 $-m 0; margin-left: -1px; overflow-y: scroll; + .page-content { + margin: 0 auto; + } } } .editor-toolbar { diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index fe3d28cbc..7ce9dbfe5 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -68,7 +68,9 @@
Preview
-
+
+
+
diff --git a/tests/Entity/MarkdownTest.php b/tests/Entity/MarkdownTest.php new file mode 100644 index 000000000..eaf4d62c3 --- /dev/null +++ b/tests/Entity/MarkdownTest.php @@ -0,0 +1,51 @@ +page = \BookStack\Page::first(); + } + + protected function setMarkdownEditor() + { + $this->setSettings(['app-editor' => 'markdown']); + } + + public function test_default_editor_is_wysiwyg() + { + $this->assertEquals(setting('app-editor'), 'wysiwyg'); + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->pageHasElement('#html-editor'); + } + + public function test_markdown_setting_shows_markdown_editor() + { + $this->setMarkdownEditor(); + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->pageNotHasElement('#html-editor') + ->pageHasElement('#markdown-editor'); + } + + public function test_markdown_content_given_to_editor() + { + $this->setMarkdownEditor(); + $mdContent = '# hello. This is a test'; + $this->page->markdown = $mdContent; + $this->page->save(); + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->seeInField('markdown', $mdContent); + } + + public function test_html_content_given_to_editor_if_no_markdown() + { + $this->setMarkdownEditor(); + $this->asAdmin()->visit($this->page->getUrl() . '/edit') + ->seeInField('markdown', $this->page->html); + } + +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 567dc93ec..f46d73e04 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -170,4 +170,28 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase $this->visit($link->link()->getUri()); return $this; } + + /** + * Check if the page contains the given element. + * @param string $selector + * @return bool + */ + protected function pageHasElement($selector) + { + $elements = $this->crawler->filter($selector); + $this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector); + return $this; + } + + /** + * Check if the page contains the given element. + * @param string $selector + * @return bool + */ + protected function pageNotHasElement($selector) + { + $elements = $this->crawler->filter($selector); + $this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector); + return $this; + } }