Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,33 +190,40 @@ protected function _deleteNodeModules(): bool
protected function _runNPMInstall(array &$output, int &$return, ConsoleIo $io, bool $useLatest = false): void
{
$pluginPath = Plugin::path('BootstrapUI');
$previousCwd = getcwd();
if (!$this->_changeWorkingDirectory($pluginPath)) {
$io->error("Could not change into plugin directory `$pluginPath`.");
$this->abort();
}

$args = [];
if ($useLatest) {
$args[] = '--package-lock false';
}
switch ($io->level()) {
case ConsoleIo::QUIET:
if ($this->_isWindows()) {
$null = 'NUL';
} else {
$null = '/dev/null';
}

$args[] = "--silent > $null";
break;
try {
$args = [];
if ($useLatest) {
$args[] = '--package-lock false';
}
switch ($io->level()) {
case ConsoleIo::QUIET:
if ($this->_isWindows()) {
$null = 'NUL';
} else {
$null = '/dev/null';
}

$args[] = "--silent > $null";
break;

case ConsoleIo::VERBOSE:
$args[] = '--verbose';
break;
}
$args = implode(' ', $args);

case ConsoleIo::VERBOSE:
$args[] = '--verbose';
break;
exec("npm install $args", $output, $return);
} finally {
if ($previousCwd !== false) {
$this->_changeWorkingDirectory($previousCwd);
}
}
$args = implode(' ', $args);

exec("npm install $args", $output, $return);
}

/**
Expand Down
42 changes: 27 additions & 15 deletions src/Command/ModifyViewCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,33 @@ protected function _modifyView(string $filePath): bool
return false;
}

$content = str_replace(
'use Cake\\View\\View',
'use BootstrapUI\\View\\UIView',
$content,
);
$content = str_replace(
'class AppView extends View',
'class AppView extends UIView',
$content,
);
$content = str_replace(
" public function initialize(): void\n {\n",
" public function initialize(): void\n {\n parent::initialize();\n",
$content,
);
$original = $content;

if (!str_contains($content, 'use BootstrapUI\\View\\UIView')) {
$content = str_replace(
'use Cake\\View\\View',
'use BootstrapUI\\View\\UIView',
$content,
);
}
if (!str_contains($content, 'class AppView extends UIView')) {
$content = str_replace(
'class AppView extends View',
'class AppView extends UIView',
$content,
);
}
if (!str_contains($content, 'parent::initialize();')) {
$content = str_replace(
" public function initialize(): void\n {\n",
" public function initialize(): void\n {\n parent::initialize();\n",
$content,
);
}

if ($content === $original) {
return false;
}

return $this->_writeFile($filePath, $content);
}
Expand Down
5 changes: 3 additions & 2 deletions src/View/Helper/BreadcrumbsHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,17 @@ protected function _markActiveCrumb(): void

$key = null;
if ($this->getConfig('ariaCurrent') === 'lastWithLink') {
foreach (array_reverse($this->crumbs, true) as $key => $crumb) {
foreach (array_reverse($this->crumbs, true) as $i => $crumb) {
if (isset($crumb['url'])) {
$key = $i;
break;
}
}
} else {
$key = count($this->crumbs) - 1;
}

if (!$key) {
if ($key === null) {
return;
}

Expand Down
80 changes: 41 additions & 39 deletions src/View/Helper/FormHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -479,50 +479,52 @@ public function control(string $fieldName, array $options = []): string
$options['templates'] = [];
}

switch ($options['type']) {
case 'checkbox':
case 'radio':
case 'select':
case 'range':
$function = '_' . $options['type'] . 'Options';
$options = $this->{$function}($fieldName, $options);
break;

default:
$options = $this->_labelOptions($fieldName, $options);
break;
}
try {
switch ($options['type']) {
case 'checkbox':
case 'radio':
case 'select':
case 'range':
$function = '_' . $options['type'] . 'Options';
$options = $this->{$function}($fieldName, $options);
break;

default:
$options = $this->_labelOptions($fieldName, $options);
break;
}

$options = $this->_spacingOptions($fieldName, $options);
$options = $this->_containerOptions($fieldName, $options);
$options = $this->_feedbackStyleOptions($fieldName, $options);
$options = $this->_ariaOptions($fieldName, $options);
$options = $this->_placeholderOptions($fieldName, $options);
$options = $this->_helpOptions($fieldName, $options);
$options = $this->_tooltipOptions($fieldName, $options);
$options = $this->_spacingOptions($fieldName, $options);
$options = $this->_containerOptions($fieldName, $options);
$options = $this->_feedbackStyleOptions($fieldName, $options);
$options = $this->_ariaOptions($fieldName, $options);
$options = $this->_placeholderOptions($fieldName, $options);
$options = $this->_helpOptions($fieldName, $options);
$options = $this->_tooltipOptions($fieldName, $options);

if (
isset($options['append']) ||
isset($options['prepend'])
) {
$options['injectErrorClass'] = $this->getConfig('templates.errorClass');
}

unset(
$options['formGroupPosition'],
$options['feedbackStyle'],
$options['spacing'],
$options['inline'],
$options['nestedInput'],
$options['switch'],
);
if (
isset($options['append']) ||
isset($options['prepend'])
) {
$options['injectErrorClass'] = $this->getConfig('templates.errorClass');
}

$result = parent::control($fieldName, $options);
unset(
$options['formGroupPosition'],
$options['feedbackStyle'],
$options['spacing'],
$options['inline'],
$options['nestedInput'],
$options['switch'],
);

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

if ($newTemplates) {
$this->templater()->pop();
$result = $this->_postProcessElement($result, $fieldName, $options);
} finally {
if ($newTemplates) {
$this->templater()->pop();
}
}

return $result;
Expand Down
6 changes: 6 additions & 0 deletions src/View/Helper/PaginatorHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ protected function _templateOptions(string $name, array $options): array
$label = $options['label'];
unset($options['label']);

// When `templates` is a file name (string), pass it through to the
// caller untouched so it can load the file via the templater.
if (is_string($options['templates'])) {
return $options;
}

$options['templates'] += [
"{$name}" => $this->getConfig("templates.{$name}", ''),
"{$name}Active" => $this->getConfig("templates.{$name}Active", ''),
Expand Down
22 changes: 22 additions & 0 deletions tests/TestCase/Command/InstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Cake\Core\Plugin;
use Cake\TestSuite\TestCase;
use Cake\Utility\Filesystem;
use ReflectionMethod;
use SplFileInfo;

class InstallCommandTest extends TestCase
Expand Down Expand Up @@ -760,6 +761,27 @@ public function testLinkPluginAssetsFailure()
);
}

/**
* `_runNPMInstall()` previously left the process cwd inside the plugin
* directory because it called `chdir()` without restoring it. Any later
* relative-path work in the same process would then resolve incorrectly.
*/
public function testRunNPMInstallRestoresCwd()
{
$cwdBefore = getcwd();
$this->assertNotFalse($cwdBefore);

$command = new InstallCommand();
$io = new ConsoleIo(new StubConsoleOutput(), new StubConsoleOutput());
$output = [];
$return = 0;

$method = new ReflectionMethod(InstallCommand::class, '_runNPMInstall');
$method->invokeArgs($command, [&$output, &$return, $io, false]);

$this->assertSame($cwdBefore, getcwd(), 'Working directory was not restored after _runNPMInstall.');
}

public function testHelp()
{
$this->exec('bootstrap install --help');
Expand Down
51 changes: 49 additions & 2 deletions tests/TestCase/Command/ModifyViewCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,39 @@ public function testPathIsNotAFile()
);
}

public function testAlreadyModifiedReportsError()
{
// Running the command on a file that has already been modified
// must be a no-op (no duplicate `parent::initialize();`) and report
// an error instead of silently succeeding.
/** @var \BootstrapUI\Command\ModifyViewCommand|\PHPUnit\Framework\MockObject\MockObject $command */
$command = $this
->getMockBuilder(ModifyViewCommand::class)
->onlyMethods(['_writeFile'])
->getMock();

$command
->expects($this->never())
->method('_writeFile');

$out = new StubConsoleOutput();
$err = new StubConsoleOutput();
$io = new ConsoleIo($out, $err);

try {
$result = $command->run([], $io);
} catch (StopException $exception) {
$result = $exception->getCode();
}

$filePath = APP . 'View' . DS . 'AppView.php';
$this->assertEquals(Command::CODE_ERROR, $result);
$this->assertEquals(
["<error>Could not modify `$filePath`.</error>"],
$err->messages(),
);
}

public function testFileCannotBeRead()
{
/** @var \BootstrapUI\Command\ModifyViewCommand|\PHPUnit\Framework\MockObject\MockObject $command */
Expand Down Expand Up @@ -179,6 +212,22 @@ public function testFileCannotBeRead()

public function testFileCannotBeWritten()
{
// _modifyView only calls _writeFile when the file content actually
// needs changes, so swap in an unmodified skeleton for this test.
$comparisonsPath =
Plugin::path('BootstrapUI') . 'tests' . DS . 'comparisons' . DS . 'Command' . DS . 'ModifyView' . DS;

$filePath = APP . 'View' . DS . 'AppView.php';

copy(
$filePath,
APP . 'View' . DS . 'AppView.php.backup',
);
copy(
$comparisonsPath . 'AppView.skeleton.php',
$filePath,
);

/** @var \BootstrapUI\Command\ModifyViewCommand|\PHPUnit\Framework\MockObject\MockObject $command */
$command = $this
->getMockBuilder(ModifyViewCommand::class)
Expand All @@ -200,8 +249,6 @@ public function testFileCannotBeWritten()
$result = $exception->getCode();
}

$filePath = APP . 'View' . DS . 'AppView.php';

$this->assertEquals(Command::CODE_ERROR, $result);
$this->assertEquals(
['<info>Modifying view...</info>'],
Expand Down
20 changes: 20 additions & 0 deletions tests/TestCase/View/Helper/BreadcrumbsHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,24 @@ public function testNoCrumbs()
$result = $this->Breadcrumbs->render();
$this->assertEmpty($result);
}

public function testSingleCrumbIsMarkedActive()
{
$result = $this->Breadcrumbs
->add('only')
->render();

$expected = [
'nav' => ['aria-label' => 'breadcrumb'],
'ol' => ['class' => 'breadcrumb'],
['li' => ['class' => 'breadcrumb-item active', 'aria-current' => 'page']],
['span' => true],
'only',
'/span',
'/li',
'/ol',
'/nav',
];
$this->assertHtml($expected, $result);
}
}
18 changes: 18 additions & 0 deletions tests/TestCase/View/Helper/PaginatorHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,24 @@ public function testFirstCustomTemplate()
$this->assertHtml($expected, $result);
}

/**
* The `templates` option is documented as accepting a file name in addition
* to an array. Passing a string previously crashed in `_templateOptions`
* which tried to array-merge it.
*/
public function testFirstCustomTemplateFile()
{
$result = $this->Paginator->first('«', [
'templates' => 'paginator_templates',
]);
$expected = [
['a' => ['data-from-file' => '1', 'href' => '/Clients/index']],
'«',
'/a',
];
$this->assertHtml($expected, $result);
}

public function testLast()
{
$result = $this->Paginator->last();
Expand Down
5 changes: 5 additions & 0 deletions tests/test_app/config/paginator_templates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
return [
'first' => '<a data-from-file="1" href="{{url}}">{{text}}</a>',
'last' => '<a data-from-file="1" href="{{url}}">{{text}}</a>',
];
Loading