Completed
Push — fix-2494 ( 3153ee )
by Sam
07:19
created

HTMLEditorFieldTest   B

Complexity

Total Complexity 14

Size/Duplication

Total Lines 272
Duplicated Lines 6.62 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
dl 18
loc 272
rs 7.8571
c 0
b 0
f 0
wmc 14
lcom 1
cbo 17

12 Methods

Rating   Name   Duplication   Size   Complexity  
B setUp() 0 29 2
A tearDown() 0 5 1
A testCasting() 0 17 1
A testBasicSaving() 0 13 1
A testNullSaving() 0 9 1
B testResizedImageInsertion() 0 32 1
A testMultiLineSaving() 7 8 1
A testSavingLinksWithoutHref() 11 14 1
A testGetAnchors() 0 48 2
A testHTMLEditorFieldFileLocal() 0 8 1
A testHTMLEditorFieldFileRemote() 0 8 1
A testReadonlyField() 0 52 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
namespace SilverStripe\Forms\Tests\HTMLEditor;
4
5
use Page;
6
use SilverStripe\Assets\File;
7
use SilverStripe\Assets\FileNameFilter;
8
use SilverStripe\Assets\Filesystem;
9
use SilverStripe\Assets\Folder;
10
use SilverStripe\Assets\Image;
11
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\HTTPRequest;
14
use SilverStripe\Core\Config\Config;
15
use SilverStripe\Dev\CSSContentParser;
16
use SilverStripe\Dev\FunctionalTest;
17
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
18
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Toolbar;
19
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Image;
20
use SilverStripe\Forms\HTMLReadonlyField;
21
use SilverStripe\Forms\Tests\HTMLEditor\HTMLEditorFieldTest\DummyMediaFormFieldExtension;
22
use SilverStripe\Forms\Tests\HTMLEditor\HTMLEditorFieldTest\TestObject;
23
use SilverStripe\ORM\FieldType\DBHTMLText;
24
use SilverStripe\Forms\HTMLEditor\TinyMCEConfig;
25
26
class HTMLEditorFieldTest extends FunctionalTest
27
{
28
29
    protected static $fixture_file = 'HTMLEditorFieldTest.yml';
30
31
    protected static $use_draft_site = true;
32
33
    protected static $required_extensions= array(
34
        HTMLEditorField_Toolbar::class => array(
35
            DummyMediaFormFieldExtension::class,
36
        ),
37
    );
38
39
    protected static $extra_dataobjects = [
40
        TestObject::class,
41
    ];
42
43
    protected function setUp()
44
    {
45
        parent::setUp();
46
47
        // Set backend root to /HTMLEditorFieldTest
48
        TestAssetStore::activate('HTMLEditorFieldTest');
49
50
        // Set the File Name Filter replacements so files have the expected names
51
        Config::inst()->update(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
52
            FileNameFilter::class,
53
            'default_replacements',
54
            array(
55
            '/\s/' => '-', // remove whitespace
56
            '/_/' => '-', // underscores to dashes
57
            '/[^A-Za-z0-9+.\-]+/' => '', // remove non-ASCII chars, only allow alphanumeric plus dash and dot
58
            '/[\-]{2,}/' => '-', // remove duplicate dashes
59
            '/^[\.\-_]+/' => '', // Remove all leading dots, dashes or underscores
60
            )
61
        );
62
63
        // Create a test files for each of the fixture references
64
        $files = File::get()->exclude('ClassName', Folder::class);
65
        foreach ($files as $file) {
66
            $fromPath = __DIR__ . '/HTMLEditorFieldTest/images/' . $file->Name;
67
            $destPath = TestAssetStore::getLocalPath($file); // Only correct for test asset store
68
            Filesystem::makeFolder(dirname($destPath));
69
            copy($fromPath, $destPath);
70
        }
71
    }
72
73
    protected function tearDown()
74
    {
75
        TestAssetStore::reset();
76
        parent::tearDown();
77
    }
78
79
    public function testCasting()
80
    {
81
        // Shim TinyMCE so silverstripe/admin doesn't have to be installed
82
        TinyMCEConfig::config()->set('base_dir', 'test');
83
        HtmlEditorField::config()->set('use_gzip', false);
84
85
        // Test special characters
86
        $inputText = "These are some unicodes: ä, ö, & ü";
87
        $field = new HTMLEditorField("Test", "Test");
88
        $field->setValue($inputText);
89
        $this->assertContains('These are some unicodes: &auml;, &ouml;, &amp; &uuml;', $field->Field());
90
        // Test shortcodes
91
        $inputText = "Shortcode: [file_link id=4]";
92
        $field = new HTMLEditorField("Test", "Test");
93
        $field->setValue($inputText);
94
        $this->assertContains('Shortcode: [file_link id=4]', $field->Field());
95
    }
96
97
    public function testBasicSaving()
98
    {
99
        $obj = new TestObject();
100
        $editor   = new HTMLEditorField('Content');
101
102
        $editor->setValue('<p class="foo">Simple Content</p>');
103
        $editor->saveInto($obj);
104
        $this->assertEquals('<p class="foo">Simple Content</p>', $obj->Content, 'Attributes are preserved.');
105
106
        $editor->setValue('<p>Unclosed Tag');
107
        $editor->saveInto($obj);
108
        $this->assertEquals('<p>Unclosed Tag</p>', $obj->Content, 'Unclosed tags are closed.');
109
    }
110
111
    public function testNullSaving()
112
    {
113
        $obj = new TestObject();
114
        $editor = new HTMLEditorField('Content');
115
116
        $editor->setValue(null);
117
        $editor->saveInto($obj);
118
        $this->assertEquals('', $obj->Content, "Doesn't choke on empty/null values.");
119
    }
120
121
    public function testResizedImageInsertion()
122
    {
123
        $obj = new TestObject();
124
        $editor = new HTMLEditorField('Content');
125
126
        $fileID = $this->idFromFixture(Image::class, 'example_image');
127
        $editor->setValue(
128
            sprintf(
129
                '[image src="assets/example.jpg" width="10" height="20" id="%d"]',
130
                $fileID
131
            )
132
        );
133
        $editor->saveInto($obj);
134
135
        $parser = new CSSContentParser($obj->dbObject('Content')->forTemplate());
136
        $xml = $parser->getByXpath('//img');
137
        $this->assertEquals(
138
            'example',
139
            (string)$xml[0]['alt'],
140
            'Alt tags are added by default based on filename'
141
        );
142
        $this->assertEquals('', (string)$xml[0]['title'], 'Title tags are added by default.');
143
        $this->assertEquals(10, (int)$xml[0]['width'], 'Width tag of resized image is set.');
144
        $this->assertEquals(20, (int)$xml[0]['height'], 'Height tag of resized image is set.');
145
146
        $neededFilename
147
            = '/assets/HTMLEditorFieldTest/f5c7c2f814/example__ResizedImageWyIxMCIsIjIwIl0.jpg';
148
149
        $this->assertEquals($neededFilename, (string)$xml[0]['src'], 'Correct URL of resized image is set.');
150
        $this->assertTrue(file_exists(BASE_PATH.DIRECTORY_SEPARATOR.$neededFilename), 'File for resized image exists');
151
        $this->assertEquals(false, $obj->HasBrokenFile, 'Referenced image file exists.');
152
    }
153
154 View Code Duplication
    public function testMultiLineSaving()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
155
    {
156
        $obj = $this->objFromFixture(TestObject::class, 'home');
157
        $editor   = new HTMLEditorField('Content');
158
        $editor->setValue('<p>First Paragraph</p><p>Second Paragraph</p>');
159
        $editor->saveInto($obj);
0 ignored issues
show
Bug introduced by
It seems like $obj defined by $this->objFromFixture(\S...tObject::class, 'home') on line 156 can be null; however, SilverStripe\Forms\HTMLE...EditorField::saveInto() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
160
        $this->assertEquals('<p>First Paragraph</p><p>Second Paragraph</p>', $obj->Content);
161
    }
162
163 View Code Duplication
    public function testSavingLinksWithoutHref()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
164
    {
165
        $obj = $this->objFromFixture(TestObject::class, 'home');
166
        $editor   = new HTMLEditorField('Content');
167
168
        $editor->setValue('<p><a name="example-anchor"></a></p>');
169
        $editor->saveInto($obj);
0 ignored issues
show
Bug introduced by
It seems like $obj defined by $this->objFromFixture(\S...tObject::class, 'home') on line 165 can be null; however, SilverStripe\Forms\HTMLE...EditorField::saveInto() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
170
171
        $this->assertEquals(
172
            '<p><a name="example-anchor"></a></p>',
173
            $obj->Content,
174
            'Saving a link without a href attribute works'
175
        );
176
    }
177
178
    public function testGetAnchors()
179
    {
180
        if (!class_exists('Page')) {
181
            $this->markTestSkipped();
182
        }
183
        $linkedPage = new Page();
184
        $linkedPage->Title = 'Dummy';
185
        $linkedPage->write();
186
187
        $html = <<<EOS
188
<div name="foo"></div>
189
<div name='bar'></div>
190
<div id="baz"></div>
191
[sitetree_link id="{$linkedPage->ID}"]
192
<div id='bam'></div>
193
<div id = "baz"></div>
194
<div id = ""></div>
195
<div id="some'id"></div>
196
<div id=bar></div>
197
EOS
198
        ;
199
        $expected = array(
200
            'foo',
201
            'bar',
202
            'baz',
203
            'bam',
204
            "some&#039;id",
205
        );
206
        $page = new Page();
207
        $page->Title = 'Test';
208
        $page->Content = $html;
209
        $page->write();
210
        $this->useDraftSite(true);
211
212
        $request = new HTTPRequest(
213
            'GET',
214
            '/',
215
            array(
216
            'PageID' => $page->ID,
217
            )
218
        );
219
220
        $toolBar = new HTMLEditorField_Toolbar(new Controller(), 'test');
221
        $toolBar->setRequest($request);
222
223
        $results = json_decode($toolBar->getanchors(), true);
224
        $this->assertEquals($expected, $results);
225
    }
226
227
    public function testHTMLEditorFieldFileLocal()
228
    {
229
        $file = new HTMLEditorField_Image('http://domain.com/folder/my_image.jpg?foo=bar');
230
        $this->assertEquals('http://domain.com/folder/my_image.jpg?foo=bar', $file->URL);
231
        $this->assertEquals('my_image.jpg', $file->Name);
232
        $this->assertEquals('jpg', $file->Extension);
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean extensions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
233
        // TODO Can't easily test remote file dimensions
234
    }
235
236
    public function testHTMLEditorFieldFileRemote()
237
    {
238
        $fileFixture = new File(array('Name' => 'my_local_image.jpg', 'Filename' => 'folder/my_local_image.jpg'));
239
        $file = new HTMLEditorField_Image('http://localdomain.com/folder/my_local_image.jpg', $fileFixture);
240
        $this->assertEquals('http://localdomain.com/folder/my_local_image.jpg', $file->URL);
241
        $this->assertEquals('my_local_image.jpg', $file->Name);
242
        $this->assertEquals('jpg', $file->Extension);
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean extensions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
243
    }
244
245
    public function testReadonlyField()
246
    {
247
        $editor = new HTMLEditorField('Content');
248
        $fileID = $this->idFromFixture(Image::class, 'example_image');
249
        $editor->setValue(
250
            sprintf(
251
                '[image src="assets/example.jpg" width="10" height="20" id="%d"]',
252
                $fileID
253
            )
254
        );
255
        /**
256
 * @var HTMLReadonlyField $readonly
257
*/
258
        $readonly = $editor->performReadonlyTransformation();
259
        /**
260
 * @var DBHTMLText $readonlyContent
261
*/
262
        $readonlyContent = $readonly->Field();
263
264
        $this->assertEquals(
265
            <<<EOS
266
<span class="readonly typography" id="Content">
267
	<img src="/assets/HTMLEditorFieldTest/f5c7c2f814/example__ResizedImageWyIxMCIsIjIwIl0.jpg" alt="example" width="10" height="20">
268
</span>
269
270
271
EOS
272
            ,
273
            $readonlyContent->getValue()
274
        );
275
276
        // Test with include input tag
277
        $readonly = $editor->performReadonlyTransformation()
278
            ->setIncludeHiddenField(true);
279
        /**
280
 * @var DBHTMLText $readonlyContent
281
*/
282
        $readonlyContent = $readonly->Field();
283
        $this->assertEquals(
284
            <<<EOS
285
<span class="readonly typography" id="Content">
286
	<img src="/assets/HTMLEditorFieldTest/f5c7c2f814/example__ResizedImageWyIxMCIsIjIwIl0.jpg" alt="example" width="10" height="20">
287
</span>
288
289
	<input type="hidden" name="Content" value="[image src=&quot;/assets/HTMLEditorFieldTest/f5c7c2f814/example.jpg&quot; width=&quot;10&quot; height=&quot;20&quot; id=&quot;{$fileID}&quot;]" />
290
291
292
EOS
293
            ,
294
            $readonlyContent->getValue()
295
        );
296
    }
297
}
298