Skip to content

Commit 589df3d

Browse files
authored
Merge pull request #434 from FriendsOfCake/bugfixes-helpers-commands
Fix five real defects in helpers and console commands
2 parents 9ca5ca0 + fa67ec5 commit 589df3d

10 files changed

Lines changed: 218 additions & 78 deletions

File tree

src/Command/InstallCommand.php

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -190,33 +190,40 @@ protected function _deleteNodeModules(): bool
190190
protected function _runNPMInstall(array &$output, int &$return, ConsoleIo $io, bool $useLatest = false): void
191191
{
192192
$pluginPath = Plugin::path('BootstrapUI');
193+
$previousCwd = getcwd();
193194
if (!$this->_changeWorkingDirectory($pluginPath)) {
194195
$io->error("Could not change into plugin directory `$pluginPath`.");
195196
$this->abort();
196197
}
197198

198-
$args = [];
199-
if ($useLatest) {
200-
$args[] = '--package-lock false';
201-
}
202-
switch ($io->level()) {
203-
case ConsoleIo::QUIET:
204-
if ($this->_isWindows()) {
205-
$null = 'NUL';
206-
} else {
207-
$null = '/dev/null';
208-
}
209-
210-
$args[] = "--silent > $null";
211-
break;
199+
try {
200+
$args = [];
201+
if ($useLatest) {
202+
$args[] = '--package-lock false';
203+
}
204+
switch ($io->level()) {
205+
case ConsoleIo::QUIET:
206+
if ($this->_isWindows()) {
207+
$null = 'NUL';
208+
} else {
209+
$null = '/dev/null';
210+
}
211+
212+
$args[] = "--silent > $null";
213+
break;
214+
215+
case ConsoleIo::VERBOSE:
216+
$args[] = '--verbose';
217+
break;
218+
}
219+
$args = implode(' ', $args);
212220

213-
case ConsoleIo::VERBOSE:
214-
$args[] = '--verbose';
215-
break;
221+
exec("npm install $args", $output, $return);
222+
} finally {
223+
if ($previousCwd !== false) {
224+
$this->_changeWorkingDirectory($previousCwd);
225+
}
216226
}
217-
$args = implode(' ', $args);
218-
219-
exec("npm install $args", $output, $return);
220227
}
221228

222229
/**

src/Command/ModifyViewCommand.php

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,33 @@ protected function _modifyView(string $filePath): bool
6060
return false;
6161
}
6262

63-
$content = str_replace(
64-
'use Cake\\View\\View',
65-
'use BootstrapUI\\View\\UIView',
66-
$content,
67-
);
68-
$content = str_replace(
69-
'class AppView extends View',
70-
'class AppView extends UIView',
71-
$content,
72-
);
73-
$content = str_replace(
74-
" public function initialize(): void\n {\n",
75-
" public function initialize(): void\n {\n parent::initialize();\n",
76-
$content,
77-
);
63+
$original = $content;
64+
65+
if (!str_contains($content, 'use BootstrapUI\\View\\UIView')) {
66+
$content = str_replace(
67+
'use Cake\\View\\View',
68+
'use BootstrapUI\\View\\UIView',
69+
$content,
70+
);
71+
}
72+
if (!str_contains($content, 'class AppView extends UIView')) {
73+
$content = str_replace(
74+
'class AppView extends View',
75+
'class AppView extends UIView',
76+
$content,
77+
);
78+
}
79+
if (!str_contains($content, 'parent::initialize();')) {
80+
$content = str_replace(
81+
" public function initialize(): void\n {\n",
82+
" public function initialize(): void\n {\n parent::initialize();\n",
83+
$content,
84+
);
85+
}
86+
87+
if ($content === $original) {
88+
return false;
89+
}
7890

7991
return $this->_writeFile($filePath, $content);
8092
}

src/View/Helper/BreadcrumbsHelper.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,17 @@ protected function _markActiveCrumb(): void
112112

113113
$key = null;
114114
if ($this->getConfig('ariaCurrent') === 'lastWithLink') {
115-
foreach (array_reverse($this->crumbs, true) as $key => $crumb) {
115+
foreach (array_reverse($this->crumbs, true) as $i => $crumb) {
116116
if (isset($crumb['url'])) {
117+
$key = $i;
117118
break;
118119
}
119120
}
120121
} else {
121122
$key = count($this->crumbs) - 1;
122123
}
123124

124-
if (!$key) {
125+
if ($key === null) {
125126
return;
126127
}
127128

src/View/Helper/FormHelper.php

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -479,50 +479,52 @@ public function control(string $fieldName, array $options = []): string
479479
$options['templates'] = [];
480480
}
481481

482-
switch ($options['type']) {
483-
case 'checkbox':
484-
case 'radio':
485-
case 'select':
486-
case 'range':
487-
$function = '_' . $options['type'] . 'Options';
488-
$options = $this->{$function}($fieldName, $options);
489-
break;
490-
491-
default:
492-
$options = $this->_labelOptions($fieldName, $options);
493-
break;
494-
}
482+
try {
483+
switch ($options['type']) {
484+
case 'checkbox':
485+
case 'radio':
486+
case 'select':
487+
case 'range':
488+
$function = '_' . $options['type'] . 'Options';
489+
$options = $this->{$function}($fieldName, $options);
490+
break;
491+
492+
default:
493+
$options = $this->_labelOptions($fieldName, $options);
494+
break;
495+
}
495496

496-
$options = $this->_spacingOptions($fieldName, $options);
497-
$options = $this->_containerOptions($fieldName, $options);
498-
$options = $this->_feedbackStyleOptions($fieldName, $options);
499-
$options = $this->_ariaOptions($fieldName, $options);
500-
$options = $this->_placeholderOptions($fieldName, $options);
501-
$options = $this->_helpOptions($fieldName, $options);
502-
$options = $this->_tooltipOptions($fieldName, $options);
497+
$options = $this->_spacingOptions($fieldName, $options);
498+
$options = $this->_containerOptions($fieldName, $options);
499+
$options = $this->_feedbackStyleOptions($fieldName, $options);
500+
$options = $this->_ariaOptions($fieldName, $options);
501+
$options = $this->_placeholderOptions($fieldName, $options);
502+
$options = $this->_helpOptions($fieldName, $options);
503+
$options = $this->_tooltipOptions($fieldName, $options);
503504

504-
if (
505-
isset($options['append']) ||
506-
isset($options['prepend'])
507-
) {
508-
$options['injectErrorClass'] = $this->getConfig('templates.errorClass');
509-
}
510-
511-
unset(
512-
$options['formGroupPosition'],
513-
$options['feedbackStyle'],
514-
$options['spacing'],
515-
$options['inline'],
516-
$options['nestedInput'],
517-
$options['switch'],
518-
);
505+
if (
506+
isset($options['append']) ||
507+
isset($options['prepend'])
508+
) {
509+
$options['injectErrorClass'] = $this->getConfig('templates.errorClass');
510+
}
519511

520-
$result = parent::control($fieldName, $options);
512+
unset(
513+
$options['formGroupPosition'],
514+
$options['feedbackStyle'],
515+
$options['spacing'],
516+
$options['inline'],
517+
$options['nestedInput'],
518+
$options['switch'],
519+
);
521520

522-
$result = $this->_postProcessElement($result, $fieldName, $options);
521+
$result = parent::control($fieldName, $options);
523522

524-
if ($newTemplates) {
525-
$this->templater()->pop();
523+
$result = $this->_postProcessElement($result, $fieldName, $options);
524+
} finally {
525+
if ($newTemplates) {
526+
$this->templater()->pop();
527+
}
526528
}
527529

528530
return $result;

src/View/Helper/PaginatorHelper.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ protected function _templateOptions(string $name, array $options): array
290290
$label = $options['label'];
291291
unset($options['label']);
292292

293+
// When `templates` is a file name (string), pass it through to the
294+
// caller untouched so it can load the file via the templater.
295+
if (is_string($options['templates'])) {
296+
return $options;
297+
}
298+
293299
$options['templates'] += [
294300
"{$name}" => $this->getConfig("templates.{$name}", ''),
295301
"{$name}Active" => $this->getConfig("templates.{$name}Active", ''),

tests/TestCase/Command/InstallCommandTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Cake\Core\Plugin;
1515
use Cake\TestSuite\TestCase;
1616
use Cake\Utility\Filesystem;
17+
use ReflectionMethod;
1718
use SplFileInfo;
1819

1920
class InstallCommandTest extends TestCase
@@ -760,6 +761,27 @@ public function testLinkPluginAssetsFailure()
760761
);
761762
}
762763

764+
/**
765+
* `_runNPMInstall()` previously left the process cwd inside the plugin
766+
* directory because it called `chdir()` without restoring it. Any later
767+
* relative-path work in the same process would then resolve incorrectly.
768+
*/
769+
public function testRunNPMInstallRestoresCwd()
770+
{
771+
$cwdBefore = getcwd();
772+
$this->assertNotFalse($cwdBefore);
773+
774+
$command = new InstallCommand();
775+
$io = new ConsoleIo(new StubConsoleOutput(), new StubConsoleOutput());
776+
$output = [];
777+
$return = 0;
778+
779+
$method = new ReflectionMethod(InstallCommand::class, '_runNPMInstall');
780+
$method->invokeArgs($command, [&$output, &$return, $io, false]);
781+
782+
$this->assertSame($cwdBefore, getcwd(), 'Working directory was not restored after _runNPMInstall.');
783+
}
784+
763785
public function testHelp()
764786
{
765787
$this->exec('bootstrap install --help');

tests/TestCase/Command/ModifyViewCommandTest.php

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,39 @@ public function testPathIsNotAFile()
141141
);
142142
}
143143

144+
public function testAlreadyModifiedReportsError()
145+
{
146+
// Running the command on a file that has already been modified
147+
// must be a no-op (no duplicate `parent::initialize();`) and report
148+
// an error instead of silently succeeding.
149+
/** @var \BootstrapUI\Command\ModifyViewCommand|\PHPUnit\Framework\MockObject\MockObject $command */
150+
$command = $this
151+
->getMockBuilder(ModifyViewCommand::class)
152+
->onlyMethods(['_writeFile'])
153+
->getMock();
154+
155+
$command
156+
->expects($this->never())
157+
->method('_writeFile');
158+
159+
$out = new StubConsoleOutput();
160+
$err = new StubConsoleOutput();
161+
$io = new ConsoleIo($out, $err);
162+
163+
try {
164+
$result = $command->run([], $io);
165+
} catch (StopException $exception) {
166+
$result = $exception->getCode();
167+
}
168+
169+
$filePath = APP . 'View' . DS . 'AppView.php';
170+
$this->assertEquals(Command::CODE_ERROR, $result);
171+
$this->assertEquals(
172+
["<error>Could not modify `$filePath`.</error>"],
173+
$err->messages(),
174+
);
175+
}
176+
144177
public function testFileCannotBeRead()
145178
{
146179
/** @var \BootstrapUI\Command\ModifyViewCommand|\PHPUnit\Framework\MockObject\MockObject $command */
@@ -179,6 +212,22 @@ public function testFileCannotBeRead()
179212

180213
public function testFileCannotBeWritten()
181214
{
215+
// _modifyView only calls _writeFile when the file content actually
216+
// needs changes, so swap in an unmodified skeleton for this test.
217+
$comparisonsPath =
218+
Plugin::path('BootstrapUI') . 'tests' . DS . 'comparisons' . DS . 'Command' . DS . 'ModifyView' . DS;
219+
220+
$filePath = APP . 'View' . DS . 'AppView.php';
221+
222+
copy(
223+
$filePath,
224+
APP . 'View' . DS . 'AppView.php.backup',
225+
);
226+
copy(
227+
$comparisonsPath . 'AppView.skeleton.php',
228+
$filePath,
229+
);
230+
182231
/** @var \BootstrapUI\Command\ModifyViewCommand|\PHPUnit\Framework\MockObject\MockObject $command */
183232
$command = $this
184233
->getMockBuilder(ModifyViewCommand::class)
@@ -200,8 +249,6 @@ public function testFileCannotBeWritten()
200249
$result = $exception->getCode();
201250
}
202251

203-
$filePath = APP . 'View' . DS . 'AppView.php';
204-
205252
$this->assertEquals(Command::CODE_ERROR, $result);
206253
$this->assertEquals(
207254
['<info>Modifying view...</info>'],

tests/TestCase/View/Helper/BreadcrumbsHelperTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,24 @@ public function testNoCrumbs()
275275
$result = $this->Breadcrumbs->render();
276276
$this->assertEmpty($result);
277277
}
278+
279+
public function testSingleCrumbIsMarkedActive()
280+
{
281+
$result = $this->Breadcrumbs
282+
->add('only')
283+
->render();
284+
285+
$expected = [
286+
'nav' => ['aria-label' => 'breadcrumb'],
287+
'ol' => ['class' => 'breadcrumb'],
288+
['li' => ['class' => 'breadcrumb-item active', 'aria-current' => 'page']],
289+
['span' => true],
290+
'only',
291+
'/span',
292+
'/li',
293+
'/ol',
294+
'/nav',
295+
];
296+
$this->assertHtml($expected, $result);
297+
}
278298
}

tests/TestCase/View/Helper/PaginatorHelperTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,24 @@ public function testFirstCustomTemplate()
474474
$this->assertHtml($expected, $result);
475475
}
476476

477+
/**
478+
* The `templates` option is documented as accepting a file name in addition
479+
* to an array. Passing a string previously crashed in `_templateOptions`
480+
* which tried to array-merge it.
481+
*/
482+
public function testFirstCustomTemplateFile()
483+
{
484+
$result = $this->Paginator->first('«', [
485+
'templates' => 'paginator_templates',
486+
]);
487+
$expected = [
488+
['a' => ['data-from-file' => '1', 'href' => '/Clients/index']],
489+
'«',
490+
'/a',
491+
];
492+
$this->assertHtml($expected, $result);
493+
}
494+
477495
public function testLast()
478496
{
479497
$result = $this->Paginator->last();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
return [
3+
'first' => '<a data-from-file="1" href="{{url}}">{{text}}</a>',
4+
'last' => '<a data-from-file="1" href="{{url}}">{{text}}</a>',
5+
];

0 commit comments

Comments
 (0)