diff --git a/package.json b/package.json index 4db1e66ab6..32863fb915 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "svgo": "svgo -f ./resources/svg/ -r", "test": "vitest run", "test-watch": "npm run test -- --watch --notify", - "frontend-dev": "vite -c vite-frontend.config.js", + "frontend-dev": "vite build -c vite-frontend.config.js --watch", "frontend-build": "vite build -c vite-frontend.config.js", "prettier:format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,blade.php,antlers.html,css}\"", "prettier:check": "prettier --check \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,blade.php,antlers.html,css}\"", diff --git a/resources/js/components/field-conditions/Validator.js b/resources/js/components/field-conditions/Validator.js index 9c59455e1f..93480e0a44 100644 --- a/resources/js/components/field-conditions/Validator.js +++ b/resources/js/components/field-conditions/Validator.js @@ -15,10 +15,10 @@ const isEmpty = (value) => { const isString = (str) => str != null && typeof str.valueOf() === 'string'; export default class { - constructor(field, values, dottedFieldPath, store) { + constructor(field, values, currentFieldPath, store) { this.field = field; this.values = values; - this.dottedFieldPath = dottedFieldPath; + this.currentFieldPath = currentFieldPath; this.store = store; this.rootValues = store ? store.values : false; this.passOnAny = false; @@ -26,6 +26,20 @@ export default class { this.converter = new Converter(); } + usingRootValues() { + if (!this.currentFieldPath) { + throw new Error('[currentFieldPath] constructor param required for `usingRootValues()`'); + } + + this.rootValues = this.values; + + if (this.currentFieldPath.includes('.')) { + return this.scopeValuesToParent(); + } + + return this; + } + passesConditions(specificConditions) { let conditions = specificConditions || this.getConditions(); @@ -185,7 +199,7 @@ export default class { getFieldValue(field) { if (field.startsWith('$parent.')) { - field = new ParentResolver(this.dottedFieldPath).resolve(field); + field = new ParentResolver(this.currentFieldPath).resolve(field); } if (field.startsWith('$root.') || field.startsWith('root.')) { @@ -249,7 +263,7 @@ export default class { values: this.values, root: this.rootValues, store: this.store, - fieldPath: this.dottedFieldPath, + fieldPath: this.currentFieldPath, }); return this.showOnPass ? passes : !passes; @@ -273,7 +287,7 @@ export default class { relativeLhsToAbsoluteFieldPath(lhs, dottedPrefix) { if (lhs.startsWith('$parent.')) { - lhs = new ParentResolver(this.dottedFieldPath).resolve(lhs); + lhs = new ParentResolver(this.currentFieldPath).resolve(lhs); } if (lhs.startsWith('$root.') || lhs.startsWith('root.')) { @@ -282,4 +296,12 @@ export default class { return dottedPrefix ? dottedPrefix + '.' + lhs : lhs; } + + scopeValuesToParent() { + let scope = this.currentFieldPath.replace(new RegExp('\.[^\.]+$'), ''); + + this.values = data_get(this.rootValues, scope); + + return this; + } } diff --git a/resources/js/frontend/components/FieldConditions.js b/resources/js/frontend/components/FieldConditions.js index f592215062..328c1ea445 100644 --- a/resources/js/frontend/components/FieldConditions.js +++ b/resources/js/frontend/components/FieldConditions.js @@ -1,7 +1,7 @@ import Validator from '../../components/field-conditions/Validator.js'; export default class { - showField(field, data) { - return new Validator(field, data).passesConditions(); + showField(conditions, data, currentFieldPath) { + return new Validator(conditions, data, currentFieldPath).usingRootValues().passesConditions(); } } diff --git a/resources/js/tests/Frontend/FieldConditionsTest.test.js b/resources/js/tests/Frontend/FieldConditionsTest.test.js index 8030086a33..52f8c11e5a 100644 --- a/resources/js/tests/Frontend/FieldConditionsTest.test.js +++ b/resources/js/tests/Frontend/FieldConditionsTest.test.js @@ -2,28 +2,67 @@ import { test, expect } from 'vitest'; import '../../frontend/helpers.js'; let formData = { + target: 'test', first_name: 'Bilbo', last_name: 'Baggins', hobby: '', bio: null, + group_field: { + dwelling: 'Bag End', + village: 'Hobbiton', + nested_group_field: { + birthday: 'Sept 22', + age: '111', + }, + }, +}; + +let showField = function (conditions, dottedFieldPath = null) { + return Statamic.$conditions.showField(conditions, formData, dottedFieldPath || 'target'); }; test('it shows field by default', () => { - expect(Statamic.$conditions.showField([], formData)).toBe(true); - expect(Statamic.$conditions.showField({}, formData)).toBe(true); + expect(showField([])).toBe(true); + expect(showField({})).toBe(true); }); test('it can show field based on empty checks', () => { - expect(Statamic.$conditions.showField({ if: { hobby: 'empty' } }, formData)).toBe(true); - expect(Statamic.$conditions.showField({ if: { bio: 'empty' } }, formData)).toBe(true); - expect(Statamic.$conditions.showField({ if: { first_name: 'empty' } }, formData)).toBe(false); - expect(Statamic.$conditions.showField({ if: { first_name: 'not empty' } }, formData)).toBe(true); + expect(showField({ if: { hobby: 'empty' } })).toBe(true); + expect(showField({ if: { bio: 'empty' } })).toBe(true); + expect(showField({ if: { first_name: 'empty' } })).toBe(false); + expect(showField({ if: { first_name: 'not empty' } })).toBe(true); }); test('it can show field if multiple conditions are met', () => { - expect(Statamic.$conditions.showField({ if: { first_name: 'Bilbo', last_name: 'Baggins' } }, formData)).toBe(true); - expect(Statamic.$conditions.showField({ if: { first_name: 'Frodo', last_name: 'Baggins' } }, formData)).toBe(false); - expect(Statamic.$conditions.showField({ if_any: { first_name: 'Frodo', last_name: 'Baggins' } }, formData)).toBe( + expect(showField({ if: { first_name: 'Bilbo', last_name: 'Baggins' } })).toBe(true); + expect(showField({ if: { first_name: 'Frodo', last_name: 'Baggins' } })).toBe(false); + expect(showField({ if_any: { first_name: 'Frodo', last_name: 'Baggins' } })).toBe(true); +}); + +test('it can show field nested in group based on sibling field value', () => { + expect(showField({ if: { village: 'Hobbiton' } }, 'group_field.dwelling')).toBe(true); + expect(showField({ if: { village: 'Mordor' } }, 'group_field.dwelling')).toBe(false); +}); + +test('it can show deeply field nested in group based on sibling field value', () => { + expect(showField({ if: { age: 111 } }, 'group_field.nested_group_field.birthday')).toBe(true); + expect(showField({ if: { age: 112 } }, 'group_field.nested_group_field.birthday')).toBe(false); +}); + +test('it can show deeply field nested in group based on parent field value', () => { + expect(showField({ if: { '$parent.village': 'Hobbiton' } }, 'group_field.nested_group_field.birthday')).toBe(true); + expect(showField({ if: { '$parent.village': 'Mordor' } }, 'group_field.nested_group_field.birthday')).toBe(false); + expect( + showField({ if: { '$parent.$parent.first_name': 'not empty' } }, 'group_field.nested_group_field.birthday'), + ).toBe(true); + expect(showField({ if: { '$parent.$parent.hobby': 'not empty' } }, 'group_field.nested_group_field.birthday')).toBe( + false, + ); +}); + +test('it can show deeply field nested in group based on root field value', () => { + expect(showField({ if: { '$root.first_name': 'not empty' } }, 'group_field.nested_group_field.birthday')).toBe( true, ); + expect(showField({ if: { '$root.hobby': 'not empty' } }, 'group_field.nested_group_field.birthday')).toBe(false); }); diff --git a/resources/views/extend/forms/fields/assets.antlers.html b/resources/views/extend/forms/fields/assets.antlers.html index 3b5e1d1ea5..4e88a6b117 100644 --- a/resources/views/extend/forms/fields/assets.antlers.html +++ b/resources/views/extend/forms/fields/assets.antlers.html @@ -1,7 +1,7 @@ + {{ foreach:options as="option|label" }} + {{ fields scope="__field" }} + {{ slot:addContext(__field) }} + {{ /fields }} + diff --git a/resources/views/extend/forms/fields/integer.antlers.html b/resources/views/extend/forms/fields/integer.antlers.html index 8392cb99a9..299b37e20a 100644 --- a/resources/views/extend/forms/fields/integer.antlers.html +++ b/resources/views/extend/forms/fields/integer.antlers.html @@ -1,6 +1,6 @@ - + defaultValue; @@ -390,4 +395,9 @@ public function extraRenderableFieldData(): array { return []; } + + public function hasJsDriverDataBinding(): bool + { + return true; + } } diff --git a/src/Fieldtypes/Checkboxes.php b/src/Fieldtypes/Checkboxes.php index 1bd7843856..024c4b978c 100644 --- a/src/Fieldtypes/Checkboxes.php +++ b/src/Fieldtypes/Checkboxes.php @@ -6,7 +6,9 @@ class Checkboxes extends Fieldtype { - use HasSelectOptions; + use HasSelectOptions { + process as traitProcess; + } protected $categories = ['controls']; protected $selectableInForms = true; @@ -51,4 +53,17 @@ protected function multiple() { return true; } + + public function preProcessValidatable($value) + { + return collect($value)->filter()->values()->all(); + } + + public function process($data) + { + return collect($this->traitProcess($data)) + ->reject(fn ($value) => $value === null) + ->values() + ->all(); + } } diff --git a/src/Fieldtypes/Group.php b/src/Fieldtypes/Group.php index d9807da6b3..96b06e91de 100644 --- a/src/Fieldtypes/Group.php +++ b/src/Fieldtypes/Group.php @@ -13,6 +13,7 @@ class Group extends Fieldtype { protected $categories = ['structured']; protected $defaultable = false; + protected $selectableInForms = true; protected function configFieldItems(): array { @@ -132,6 +133,19 @@ public function preProcessValidatable($value) ); } + public function preProcessTagRenderable($data, $recursiveCallback) + { + $field = $this->field(); + + $data['fields'] = collect($this->fields()->all()) + ->map(fn ($child) => $child->setForm($field->form())->setHandle($field->handle().'.'.$child->handle())) + ->map(fn ($child) => $recursiveCallback($child)) + ->values() + ->all(); + + return $data; + } + public function toGqlType() { return GraphQL::type($this->gqlItemTypeName()); @@ -152,4 +166,9 @@ private function gqlItemTypeName() return Str::studly($part); })->join('_'); } + + public function hasJsDriverDataBinding(): bool + { + return false; + } } diff --git a/src/Fieldtypes/HasSelectOptions.php b/src/Fieldtypes/HasSelectOptions.php index 7e1289a3c4..b7efb8cab4 100644 --- a/src/Fieldtypes/HasSelectOptions.php +++ b/src/Fieldtypes/HasSelectOptions.php @@ -149,8 +149,6 @@ private function castToBoolean($value) return true; } elseif ($value === 'false') { return false; - } elseif ($value === 'null') { - return null; } return $value; @@ -162,8 +160,6 @@ private function castFromBoolean($value) return 'true'; } elseif ($value === false) { return 'false'; - } elseif ($value === null) { - return 'null'; } return $value; diff --git a/src/Forms/JsDrivers/AbstractJsDriver.php b/src/Forms/JsDrivers/AbstractJsDriver.php index ec2e37367a..76620170b2 100644 --- a/src/Forms/JsDrivers/AbstractJsDriver.php +++ b/src/Forms/JsDrivers/AbstractJsDriver.php @@ -2,7 +2,9 @@ namespace Statamic\Forms\JsDrivers; +use Illuminate\Support\Collection; use Statamic\Forms\Form; +use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Tags\Parameters; @@ -87,30 +89,26 @@ public function render($html) /** * Copy renderable `show_field` JS from each individual field for hardcoding field html using top-level form data. - * - * @param array $fields - * @return array */ - public function copyShowFieldToFormData($fields) + public function copyShowFieldToFormData(array $fields): array { - return $this->validateShowFieldDefined(collect($fields))->pluck('show_field', 'handle')->all(); + return $this + ->flattenFields($fields) + ->each(fn ($field) => $this->validateShowFieldDefined($field)) + ->pluck('show_field', 'handle') + ->all(); } /** * Validate that `show_field` is defined in `addToRenderableFieldData()` output. * - * @param \Illuminate\Support\Collection $fields - * @return \Illuminate\Support\Collection - * * @throws \Exception */ - protected function validateShowFieldDefined($fields) + protected function validateShowFieldDefined(array $field): void { - return $fields->each(function ($field) { - if (! isset($field['show_field'])) { - throw new \Exception('JS driver requires [show_field] to be defined in [addToRenderableFieldData()] output!'); - } - }); + if (! isset($field['show_field'])) { + throw new \Exception('JS driver requires [show_field] to be defined in [addToRenderableFieldData()] output!'); + } } /** @@ -127,33 +125,34 @@ protected function validateRenderMethodReturnsHtml() /** * Get initial form data. - * - * @return array */ - protected function getInitialFormData() + protected function getInitialFormData(): array { - $oldValues = collect(old()); - return $this->form ->blueprint() ->fields() + ->addValues(old() ?? []) ->preProcess() ->values() ->when($this->form->honeypot(), fn ($fields, $honeypot) => $fields->merge([$honeypot => null])) - ->map(function ($defaultProcessedValue, $handle) use ($oldValues) { - return $oldValues->has($handle) - ? $oldValues->get($handle) - : $defaultProcessedValue; - }) ->all(); } + /** + * Recursively get flattened fields collection. + */ + protected function flattenFields(array|Collection $fields): Collection + { + return collect($fields)->flatMap(fn ($field) => [ + $field, + ...$this->flattenFields(Arr::get($field, 'fields') ?? [])->all(), + ]); + } + /** * Get JS driver handle from class name. - * - * @return string */ - public static function handle() + public static function handle(): string { $className = collect(explode('\\', static::class))->last(); @@ -163,7 +162,7 @@ public static function handle() /** * Register driver with Statamic. */ - public static function register() + public static function register(): void { if (! app()->has('statamic.form-js-drivers')) { return; diff --git a/src/Forms/JsDrivers/Alpine.php b/src/Forms/JsDrivers/Alpine.php index e69d824f57..9f7256b0dc 100644 --- a/src/Forms/JsDrivers/Alpine.php +++ b/src/Forms/JsDrivers/Alpine.php @@ -2,6 +2,7 @@ namespace Statamic\Forms\JsDrivers; +use Statamic\Fields\Field; use Statamic\Statamic; class Alpine extends AbstractJsDriver @@ -46,7 +47,7 @@ public function addToFormAttributes() public function addToRenderableFieldData($field, $data) { return [ - 'show_field' => $this->renderAlpineShowFieldJs($field->conditions(), $this->scope), + 'show_field' => $this->renderAlpineShowFieldJs($field, $this->scope), ]; } @@ -58,9 +59,13 @@ public function addToRenderableFieldData($field, $data) */ public function addToRenderableFieldAttributes($field) { - return [ - 'x-model' => $this->getAlpineXDataKey($field->handle(), $this->scope), - ]; + $attributes = []; + + if ($field->fieldtype()->hasJsDriverDataBinding()) { + $attributes['x-model'] = $this->getAlpineXDataKey($field->handle(), $this->scope); + } + + return $attributes; } /** @@ -97,14 +102,10 @@ protected function getAlpineXDataKey($fieldHandle, $alpineScope) /** * Render alpine `x-if` show field JS logic. - * - * @param array $conditions - * @param string $alpineScope - * @return string */ - protected function renderAlpineShowFieldJs($conditions, $alpineScope) + protected function renderAlpineShowFieldJs(Field $field, ?string $alpineScope): string { - $conditionsObject = Statamic::modify($conditions)->toJson()->entities(); + $conditionsObject = Statamic::modify($field->conditions())->toJson()->entities(); $dataObject = '$data'; @@ -112,6 +113,8 @@ protected function renderAlpineShowFieldJs($conditions, $alpineScope) $dataObject .= ".{$alpineScope}"; } - return 'Statamic.$conditions.showField('.$conditionsObject.', '.$dataObject.')'; + $currentFieldPath = $field->handle(); + + return "Statamic.\$conditions.showField({$conditionsObject}, {$dataObject}, '{$currentFieldPath}')"; } } diff --git a/src/Forms/JsDrivers/JsDriver.php b/src/Forms/JsDrivers/JsDriver.php index 12e6504d37..9e8f61d642 100644 --- a/src/Forms/JsDrivers/JsDriver.php +++ b/src/Forms/JsDrivers/JsDriver.php @@ -14,5 +14,5 @@ public function addToRenderableFieldAttributes($field); public function render($html); - public function copyShowFieldToFormData($fields); + public function copyShowFieldToFormData(array $fields): array; } diff --git a/src/Forms/RenderableField.php b/src/Forms/RenderableField.php new file mode 100644 index 0000000000..1e4f0af89c --- /dev/null +++ b/src/Forms/RenderableField.php @@ -0,0 +1,60 @@ +slot = $slot; + + collect($this->data['fields'] ?? []) + ->each(fn ($field) => $field['field']->slot($slot)); + + return $this; + } + + public function toHtml(): string + { + $data = array_merge($this->data, [ + 'slot' => $this->slot, + ]); + + return static::minify( + view($this->field->fieldtype()->view(), $data)->render(), + ); + } + + public function __toString(): string + { + return $this->toHtml(); + } + + /** + * We minify renderable fieldtype html from our vendor publishable field partials, + * because it makes makes things a bit more consistent and forgiving as far as + * whitespace around textarea content, checkbox/radio labels, groups, etc. + * + * This allows us to format fieldtype partials nicely in a pleasing way that + * makes sense to devs who are publishing and overriding fieldtype html. + */ + public static function minify(string $html): string + { + // Leave whitespace around textually inline html elements. + $ignoredHtmlElements = collect(['a', 'span'])->implode('|'); + + // Trim whitespace between all other html elements. + $html = preg_replace('/\s*(<(?!\/*('.$ignoredHtmlElements.'))[^>]+>)\s*/', '$1', $html); + + return $html; + } +} diff --git a/src/Forms/RenderableFieldSlot.php b/src/Forms/RenderableFieldSlot.php new file mode 100644 index 0000000000..624fe318a1 --- /dev/null +++ b/src/Forms/RenderableFieldSlot.php @@ -0,0 +1,39 @@ +context = $context; + + return $this; + } + + public function __toString(): string + { + $context = $this->context; + + if ($this->scope) { + $context = Arr::addScope($context, $this->scope); + } + + if ($this->isBlade) { + return Blade::render($this->html, ['field' => $context]); + } + + return (string) Antlers::parse($this->html, $context); + } +} diff --git a/src/Forms/Tags.php b/src/Forms/Tags.php index f44018e7c6..83bf34f3b5 100644 --- a/src/Forms/Tags.php +++ b/src/Forms/Tags.php @@ -5,12 +5,14 @@ use DebugBar\DataCollector\ConfigCollector; use DebugBar\DebugBarException; use Statamic\Contracts\Forms\Form as FormContract; +use Statamic\Facades\Antlers; use Statamic\Facades\Blink; use Statamic\Facades\Blueprint; use Statamic\Facades\Form; use Statamic\Facades\URL; use Statamic\Forms\JsDrivers\JsDriver; use Statamic\Support\Arr; +use Statamic\Support\Html; use Statamic\Support\Str; use Statamic\Tags\Concerns; use Statamic\Tags\Tags as BaseTags; @@ -102,6 +104,7 @@ public function create() if ($jsDriver) { $attrs = array_merge($attrs, $jsDriver->addToFormAttributes($form)); } + $attrs = $this->runHooks('attrs', ['attrs' => $attrs, 'data' => $data])['attrs']; $params = []; @@ -138,6 +141,39 @@ public function create() return $html; } + /** + * Maps to {{ form:fields }}. + * + * @return string + */ + public function fields() + { + $isBlade = $this->isAntlersBladeComponent(); + + $scope = $this->params->get('scope'); + + $slot = new RenderableFieldSlot( + html: $this->content, + scope: $scope, + isBlade: $isBlade, + ); + + collect($this->context['fields']) + ->each(fn ($field) => $field['field']->slot($slot)); + + if ($isBlade) { + return $this->tagRenderer->render('@foreach($fields as $field)'.$this->content.'@endforeach', $this->context->all()); + } + + $params = ''; + + if ($scope) { + $params = Html::attributes(['scope' => $scope]); + } + + return Antlers::parse('{{ fields '.$params.' }}'.$this->content.'{{ /fields }}', $this->context->all()); + } + /** * Maps to {{ form:errors }}. * diff --git a/src/Http/Controllers/FormController.php b/src/Http/Controllers/FormController.php index 3249c68de1..45c12d5193 100644 --- a/src/Http/Controllers/FormController.php +++ b/src/Http/Controllers/FormController.php @@ -33,12 +33,6 @@ public function submit(FrontendFormRequest $request, $form) $this->validateContentType($request, $form); $values = $request->all(); - $fields->all() - ->filter(fn ($field) => $field->fieldtype()->handle() === 'checkboxes') - ->each(function ($field) use (&$values) { - return Arr::set($values, $field->handle(), collect(Arr::get($values, $field->handle(), []))->filter(fn ($value) => $value !== null)->values()->all()); - }); - $values = array_merge($values, $assets = $request->assets()); $params = collect($request->all())->filter(function ($value, $key) { return Str::startsWith($key, '_'); diff --git a/src/Tags/Concerns/RendersForms.php b/src/Tags/Concerns/RendersForms.php index 8f86e7edec..f7809e616f 100644 --- a/src/Tags/Concerns/RendersForms.php +++ b/src/Tags/Concerns/RendersForms.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Support\MessageBag; use Statamic\Fields\Field; +use Statamic\Forms\RenderableField; use Statamic\Support\Str; trait RendersForms @@ -142,7 +143,10 @@ protected function getRenderableField($field, $errorBag = 'default', $manipulate ->filter()->all(); $formHandle = $field->form()?->handle() ?? Str::slug($errorBag); + $data = array_merge($configDefaults, $field->toArray(), [ + 'handle' => $field->handle(), + 'name' => $this->convertDottedHandleToInputName($field->handle()), 'id' => $this->generateFieldId($field->handle(), $formHandle), 'instructions' => $field->instructions(), 'error' => $errors->first($field->handle()) ?: null, @@ -151,37 +155,44 @@ protected function getRenderableField($field, $errorBag = 'default', $manipulate 'value' => $value, ], $field->fieldtype()->extraRenderableFieldData()); + $data = $field + ->fieldtype() + ->preProcessTagRenderable($data, fn ($child) => $this->getRenderableField( + $child, + $errorBag, + $manipulateDataCallback, + )); + if ($manipulateDataCallback instanceof Closure) { $data = $manipulateDataCallback($data, $field); } - $data['field'] = $this->minifyFieldHtml(view($field->fieldtype()->view(), $data)->render()); + $data['field'] = new RenderableField($field, $data); return $data; } /** - * Minify field html. - * - * @param string $html - * @return string + * Generate a field id to associate input with label. */ - protected function minifyFieldHtml($html) + private function generateFieldId(string $fieldHandle, ?string $formName = null): string { - // Leave whitespace around these html elements. - $ignoredHtmlElements = collect(['a', 'span'])->implode('|'); + $formName ??= 'default'; - // Trim whitespace between all other html elements. - $html = preg_replace('/\s*(<(?!\/*('.$ignoredHtmlElements.'))[^>]+>)\s*/', '$1', $html); - - return $html; + return str_replace(['.', '_'], '-', "{$formName}-form-{$fieldHandle}-field"); } /** - * Generate a field id to associate input with label. + * Convert dotted handle to input name that can be submitted as array value in form html. */ - private function generateFieldId(string $fieldHandle, ?string $formName = null): string + protected function convertDottedHandleToInputName(string $handle): string { - return ($formName ?? 'default').'-form-'.$fieldHandle.'-field'; + $parts = collect(explode('.', $handle)); + + $first = $parts->pull(0); + + return $first.$parts + ->map(fn ($part) => '['.$part.']') + ->join(''); } } diff --git a/tests/Fieldtypes/CastsBooleansTests.php b/tests/Fieldtypes/CastsBooleansTests.php index 61943a885f..4e1b852034 100644 --- a/tests/Fieldtypes/CastsBooleansTests.php +++ b/tests/Fieldtypes/CastsBooleansTests.php @@ -15,24 +15,20 @@ public function it_casts_booleans_during_processing_when_enabled() 'options' => [ 'true' => 'Yup', 'false' => 'Nope', - 'null' => 'Dunno', 'foo' => 'Bar', ], ]); $this->assertEquals(true, $field->process('true')); $this->assertEquals(false, $field->process('false')); - $this->assertEquals(null, $field->process('null')); $this->assertEquals('foo', $field->process('foo')); $this->assertEquals('true', $field->preProcess(true)); $this->assertEquals('false', $field->preProcess(false)); - $this->assertEquals('null', $field->preProcess(null)); $this->assertEquals('foo', $field->preProcess('foo')); $this->assertEquals(['Yup'], $field->preProcessIndex(true)); $this->assertEquals(['Nope'], $field->preProcessIndex(false)); - $this->assertEquals(['Dunno'], $field->preProcessIndex(null)); $this->assertEquals(['Bar'], $field->preProcessIndex('foo')); } @@ -45,24 +41,20 @@ public function it_doesnt_cast_booleans_during_processing_when_disabled() 'options' => [ 'true' => 'Yup', 'false' => 'Nope', - 'null' => 'Dunno', 'foo' => 'Bar', ], ]); $this->assertEquals('true', $field->process('true')); $this->assertEquals('false', $field->process('false')); - $this->assertEquals('null', $field->process('null')); $this->assertEquals('foo', $field->process('foo')); $this->assertEquals(true, $field->preProcess(true)); $this->assertEquals(false, $field->preProcess(false)); - $this->assertEquals(null, $field->preProcess(null)); $this->assertEquals('foo', $field->preProcess('foo')); $this->assertEquals([true], $field->preProcessIndex(true)); $this->assertEquals([false], $field->preProcessIndex(false)); - $this->assertEquals([null], $field->preProcessIndex(null)); $this->assertEquals(['Bar'], $field->preProcessIndex('foo')); } } diff --git a/tests/Fieldtypes/CastsMultipleBooleansTests.php b/tests/Fieldtypes/CastsMultipleBooleansTests.php index 717046f9bc..922efaaf1d 100644 --- a/tests/Fieldtypes/CastsMultipleBooleansTests.php +++ b/tests/Fieldtypes/CastsMultipleBooleansTests.php @@ -16,27 +16,23 @@ public function it_casts_multiple_booleans_during_processing_when_enabled() 'options' => [ 'true' => 'Yup', 'false' => 'Nope', - 'null' => 'Dunno', 'foo' => 'Bar', ], ]); $this->assertEquals( - [true, false, null, 'foo'], - $field->process(['true', 'false', 'null', 'foo']) + [true, false, 'foo'], + $field->process(['true', 'false', 'foo']) ); $this->assertEquals( - ['true', 'false', 'null', 'foo'], - $field->preProcess([true, false, null, 'foo']) + ['true', 'false', 'foo'], + $field->preProcess([true, false, 'foo']) ); - $this->assertEquals([], $field->preProcess(null)); - $this->assertEquals(['null'], $field->preProcess([null])); - $this->assertEquals( - ['Yup', 'Nope', 'Dunno', 'Bar'], - $field->preProcessIndex([true, false, null, 'foo']) + ['Yup', 'Nope', 'Bar'], + $field->preProcessIndex([true, false, 'foo']) ); } @@ -50,24 +46,23 @@ public function it_doesnt_cast_multiple_booleans_during_processing_when_disabled 'options' => [ 'true' => 'Yup', 'false' => 'Nope', - 'null' => 'Dunno', 'foo' => 'Bar', ], ]); $this->assertEquals( - ['true', 'false', 'null', 'foo'], - $field->process(['true', 'false', 'null', 'foo']) + ['true', 'false', 'foo'], + $field->process(['true', 'false', 'foo']) ); $this->assertEquals( - [true, false, null, 'foo'], - $field->preProcess([true, false, null, 'foo']) + [true, false, 'foo'], + $field->preProcess([true, false, 'foo']) ); $this->assertEquals( - [true, false, null, 'Bar'], - $field->preProcessIndex([true, false, null, 'foo']) + [true, false, 'Bar'], + $field->preProcessIndex([true, false, 'foo']) ); } } diff --git a/tests/Fieldtypes/CheckboxesTest.php b/tests/Fieldtypes/CheckboxesTest.php index d1e6ad26fa..ef5841a30c 100644 --- a/tests/Fieldtypes/CheckboxesTest.php +++ b/tests/Fieldtypes/CheckboxesTest.php @@ -2,6 +2,7 @@ namespace Tests\Fieldtypes; +use PHPUnit\Framework\Attributes\Test; use Statamic\Fields\Field; use Statamic\Fieldtypes\Checkboxes; use Tests\TestCase; @@ -16,4 +17,11 @@ private function field($config) return $ft->setField(new Field('test', array_merge($config, ['type' => $ft->handle()]))); } + + #[Test] + public function it_filters_out_nulls() + { + $this->assertSame(['foo', 'bar'], $this->field([])->process(['foo', null, 'bar'])); + $this->assertSame(['foo', 'bar'], $this->field([])->preProcessValidatable(['foo', null, 'bar'])); + } } diff --git a/tests/Fieldtypes/LabeledValueTests.php b/tests/Fieldtypes/LabeledValueTests.php index cc189affde..dd9bf34e33 100644 --- a/tests/Fieldtypes/LabeledValueTests.php +++ b/tests/Fieldtypes/LabeledValueTests.php @@ -223,14 +223,13 @@ public function it_augments_to_a_LabeledValue_object_with_boolean_casting_and_a_ 'options' => [ 'true' => 'Yup', 'false' => 'Nope', - 'null' => 'Dunno', ], ]); $augmented = $field->augment(null); $this->assertInstanceOf(LabeledValue::class, $augmented); $this->assertNull($augmented->value()); - $this->assertEquals('Dunno', $augmented->label()); + $this->assertNull($augmented->label()); $augmented = $field->augment(false); $this->assertInstanceOf(LabeledValue::class, $augmented); diff --git a/tests/Fieldtypes/MultipleLabeledValueTests.php b/tests/Fieldtypes/MultipleLabeledValueTests.php index 85a1efa51a..fd71bf7157 100644 --- a/tests/Fieldtypes/MultipleLabeledValueTests.php +++ b/tests/Fieldtypes/MultipleLabeledValueTests.php @@ -29,11 +29,10 @@ public function it_augments_multiple_enabled_to_an_array_of_LabeledValue_equival $this->assertEquals([ ['key' => 'au', 'value' => 'au', 'label' => 'Australia'], ['key' => 'us', 'value' => 'us', 'label' => 'USA'], - ['key' => null, 'value' => null, 'label' => null], ['key' => false, 'value' => false, 'label' => false], ['key' => true, 'value' => true, 'label' => true], ['key' => 'missing', 'value' => 'missing', 'label' => 'missing'], - ], $field->augment(['au', 'us', null, false, true, 'missing'])); + ], $field->augment(['au', 'us', false, true, 'missing'])); } #[Test] @@ -51,11 +50,10 @@ public function it_augments_multiple_enabled_to_an_array_of_LabeledValue_equival $this->assertEquals([ ['key' => 2, 'value' => 2, 'label' => 'Canada'], - ['key' => null, 'value' => null, 'label' => null], ['key' => false, 'value' => false, 'label' => false], ['key' => true, 'value' => true, 'label' => true], ['key' => 'missing', 'value' => 'missing', 'label' => 'missing'], - ], $field->augment([2, null, false, true, 'missing'])); + ], $field->augment([2, false, true, 'missing'])); $this->assertEquals([ ['key' => 2, 'value' => 2, 'label' => 'Canada'], @@ -118,16 +116,14 @@ public function it_augments_multiple_enabled_to_an_array_of_LabeledValue_equival 'options' => [ 'true' => 'Yup', 'false' => 'Nope', - 'null' => 'Dunno', ], ]); $this->assertEquals([ - ['key' => null, 'value' => null, 'label' => 'Dunno'], ['key' => false, 'value' => false, 'label' => 'Nope'], ['key' => true, 'value' => true, 'label' => 'Yup'], ['key' => 'missing', 'value' => 'missing', 'label' => 'missing'], - ], $field->augment([null, false, true, 'missing'])); + ], $field->augment([false, true, 'missing'])); } #[Test] @@ -141,11 +137,10 @@ public function it_augments_multiple_enabled_to_an_array_of_LabeledValue_equival ]); $this->assertEquals([ - ['key' => null, 'value' => null, 'label' => null], ['key' => false, 'value' => false, 'label' => false], ['key' => true, 'value' => true, 'label' => true], ['key' => 'missing', 'value' => 'missing', 'label' => 'missing'], - ], $field->augment([null, false, true, 'missing'])); + ], $field->augment([false, true, 'missing'])); } public static function noMultipleOptionsProvider() diff --git a/tests/Tags/Concerns/RendersFormsTest.php b/tests/Tags/Concerns/RendersFormsTest.php index a83d218ae8..2774f485bf 100644 --- a/tests/Tags/Concerns/RendersFormsTest.php +++ b/tests/Tags/Concerns/RendersFormsTest.php @@ -6,13 +6,17 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Antlers; use Statamic\Fields\Field; +use Statamic\Forms\RenderableField; use Statamic\Support\Arr; use Statamic\Tags\Concerns; use Statamic\Tags\Tags; +use Tests\FakesViews; use Tests\TestCase; class RendersFormsTest extends TestCase { + use FakesViews; + const MISSING = 'field is missing from request'; private $tag; @@ -68,9 +72,9 @@ public function it_renders_form_close_tag() } #[Test] - public function it_minifies_space_between_field_html_elements() + public function renderable_fields_minify_space_between_field_html_elements_when_casting_to_string() { - $fields = <<<'EOT' + $html = <<<'HTML' One @@ -91,14 +95,20 @@ public function it_minifies_space_between_field_html_elements() Start with and end with a link -EOT; +HTML; + + $this->withFakeViews(); + $this->viewShouldReturnRaw('statamic::forms.fields.text', $html); $expected = 'OneTwoOption with link text or style classIntentionallytightlink orstyleclassSome link or styled textStart with and end with a link'; - $this->assertEquals($expected, $this->tag->minifyFieldHtml($fields)); + $field = $this->createField('text')['field']; + + $this->assertInstanceOf(RenderableField::class, $field); + $this->assertEquals($expected, (string) $field); } - private function createField($type, $value, $default, $old, $config = []) + private function createField($type, $value = null, $default = null, $old = null, $config = []) { $config = array_merge($config, ['type' => $type]); diff --git a/tests/Tags/Form/FormCreateAlpineTest.php b/tests/Tags/Form/FormCreateAlpineTest.php index 58f84058e0..194433698a 100644 --- a/tests/Tags/Form/FormCreateAlpineTest.php +++ b/tests/Tags/Form/FormCreateAlpineTest.php @@ -37,36 +37,76 @@ class FormCreateAlpineTest extends FormTestCase ], ], [ - 'handle' => 'fav_animals', + 'handle' => 'likes_animals', 'field' => [ - 'type' => 'checkboxes', - 'display' => 'Fav Animals', - 'options' => [ - 'cat' => 'Cat', - 'armadillo' => 'Armadillo', - 'rat' => 'Rat', - ], + 'type' => 'toggle', + 'display' => 'Likes Animals', ], ], [ - 'handle' => 'fav_colour', + 'handle' => 'my_favourites', 'field' => [ - 'type' => 'radio', - 'display' => 'Fav Colour', - 'options' => [ - 'red' => 'Red', - 'blue' => 'Blue', + 'type' => 'group', + 'display' => 'Group One', + 'if' => [ + 'name' => 'not empty', ], - ], - ], - [ - 'handle' => 'fav_subject', - 'field' => [ - 'type' => 'select', - 'display' => 'Fav Subject', - 'options' => [ - 'math' => 'Math', - 'english' => 'English', + 'fields' => [ + [ + 'handle' => 'favourite_animals', + 'field' => [ + 'type' => 'checkboxes', + 'display' => 'Favourite Animals', + 'options' => [ + 'cat' => 'Cat', + 'armadillo' => 'Armadillo', + 'rat' => 'Rat', + ], + 'if' => [ + '$root.likes_animals' => 'is true', + ], + ], + ], + [ + 'handle' => 'non_favourite_animals', + 'field' => [ + 'type' => 'checkboxes', + 'display' => 'Non-Favourite Animals', + 'options' => [ + 'cat' => 'Cat', + 'armadillo' => 'Armadillo', + 'rat' => 'Rat', + ], + 'if' => [ + 'favourite_animals' => 'not empty', + ], + ], + ], + [ + 'handle' => 'favourite_colour', + 'field' => [ + 'type' => 'radio', + 'display' => 'Favourite Colour', + 'options' => [ + 'red' => 'Red', + 'blue' => 'Blue', + ], + ], + ], + [ + 'handle' => 'favourite_subject', + 'field' => [ + 'type' => 'select', + 'display' => 'Favourite Subject', + 'options' => [ + 'math' => 'Math', + 'english' => 'English', + ], + 'if' => [ + '$parent.message' => 'not empty', + ], + ], + ], ], ], ], @@ -95,9 +135,13 @@ public function it_renders_x_data_on_form_tag() 'name' => null, 'email' => null, 'message' => null, - 'fav_animals' => [], - 'fav_colour' => null, - 'fav_subject' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => null, + 'favourite_subject' => null, + ], 'winnie' => null, ]); @@ -121,9 +165,13 @@ public function it_renders_x_data_with_old_data_on_form_tag() 'name' => 'Frodo Braggins', 'email' => null, 'message' => null, - 'fav_animals' => [], - 'fav_colour' => null, - 'fav_subject' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => null, + 'favourite_subject' => null, + ], 'winnie' => null, ]); @@ -142,9 +190,81 @@ public function it_renders_scoped_x_data_on_form_tag() 'name' => null, 'email' => null, 'message' => null, - 'fav_animals' => [], - 'fav_colour' => null, - 'fav_subject' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => null, + 'favourite_subject' => null, + ], + 'winnie' => null, + ], + ]); + + $expected = ''; + + $this->assertStringContainsString($expected, $output); + } + + #[Test] + public function it_renders_x_data_with_old_data_on_form_tag_when_only_one_nested_field_is_submitted() + { + $this + ->post('/!/forms/contact', [ + 'name' => 'Frodo Braggins', + 'my_favourites' => [ + 'favourite_colour' => 'red', + ], + ]) + ->assertSessionHasErrors(['email', 'message'], null, 'form.contact'); + + $output = $this->tag('{{ form:contact js="alpine" }}{{ /form:contact }}'); + + $expectedXData = $this->jsonEncode([ + 'name' => 'Frodo Braggins', + 'email' => null, + 'message' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => 'red', + 'favourite_subject' => null, + ], + 'winnie' => null, + ]); + + $expected = ''; + + $this->assertStringContainsString($expected, $output); + } + + #[Test] + public function it_renders_scoped_x_data_with_old_data_on_form_tag_when_only_one_nested_field_is_submitted() + { + $this + ->post('/!/forms/contact', [ + 'name' => 'Frodo Braggins', + 'my_favourites' => [ + 'favourite_colour' => 'red', + ], + ]) + ->assertSessionHasErrors(['email', 'message'], null, 'form.contact'); + + $output = $this->tag('{{ form:contact js="alpine:my_form" }}{{ /form:contact }}'); + + $expectedXData = $this->jsonEncode([ + 'my_form' => [ + 'name' => 'Frodo Braggins', + 'email' => null, + 'message' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => 'red', + 'favourite_subject' => null, + ], 'winnie' => null, ], ]); @@ -171,9 +291,13 @@ public function it_renders_scoped_x_data_with_old_data_on_form_tag() 'name' => 'Frodo Braggins', 'email' => null, 'message' => null, - 'fav_animals' => ['cat'], - 'fav_colour' => null, - 'fav_subject' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => null, + 'favourite_subject' => null, + ], 'winnie' => null, ], ]); @@ -222,32 +346,91 @@ public function it_renders_proper_x_data_for_multiple_assets_field() } #[Test] - public function it_renders_show_field_js() + public function it_renders_show_field_js_recursively() + { + $outputWithJsDisabled = $this->tag('{{ form:contact }}{{ /form:contact }}'); + + $output = $this->tag(<<<'EOT' +{{ form:contact js="alpine" }} + {{ form:fields }} + {{ field }} + {{ /form:fields }} +{{ /form:contact }} +EOT + ); + + preg_match_all('//U', $output, $js); + + $expected = [ + 'Statamic.$conditions.showField([], $data, \'name\')', + 'Statamic.$conditions.showField([], $data, \'email\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data, \'message\')', + 'Statamic.$conditions.showField([], $data, \'likes_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['name' => 'not empty']]).', $data, \'my_favourites\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['$root.likes_animals' => 'is true']]).', $data, \'my_favourites.favourite_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['favourite_animals' => 'not empty']]).', $data, \'my_favourites.non_favourite_animals\')', + 'Statamic.$conditions.showField([], $data, \'my_favourites.favourite_colour\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['$parent.message' => 'not empty']]).', $data, \'my_favourites.favourite_subject\')', + ]; + + $this->assertStringNotContainsString('Statamic.$conditions', $outputWithJsDisabled); + $this->assertEquals($expected, $js[1]); + } + + #[Test] + public function it_renders_scoped_show_field_js_recursively() + { + $outputWithJsDisabled = $this->tag('{{ form:contact }}{{ /form:contact }}'); + + $output = $this->tag(<<<'EOT' +{{ form:contact js="alpine:my_form" }} + {{ form:fields }} + {{ field }} + {{ /form:fields }} +{{ /form:contact }} +EOT + ); + + preg_match_all('//U', $output, $js); + + $expected = [ + 'Statamic.$conditions.showField([], $data.my_form, \'name\')', + 'Statamic.$conditions.showField([], $data.my_form, \'email\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data.my_form, \'message\')', + 'Statamic.$conditions.showField([], $data.my_form, \'likes_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['name' => 'not empty']]).', $data.my_form, \'my_favourites\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['$root.likes_animals' => 'is true']]).', $data.my_form, \'my_favourites.favourite_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['favourite_animals' => 'not empty']]).', $data.my_form, \'my_favourites.non_favourite_animals\')', + 'Statamic.$conditions.showField([], $data.my_form, \'my_favourites.favourite_colour\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['$parent.message' => 'not empty']]).', $data.my_form, \'my_favourites.favourite_subject\')', + ]; + + $this->assertStringNotContainsString('Statamic.$conditions', $outputWithJsDisabled); + $this->assertEquals($expected, $js[1]); + } + + #[Test] + public function it_renders_show_field_js_inside_legacy_fields_array() { $outputWithJsDisabled = $this->tag('{{ form:contact }}{{ /form:contact }}'); $output = $this->tag(<<<'EOT' {{ form:contact js="alpine" }} - - {{ fields }} - + {{ field }} {{ /fields }} {{ /form:contact }} EOT ); - preg_match_all('/<\/template>/U', $output, $js); + preg_match_all('//U', $output, $js); $expected = [ - 'Statamic.$conditions.showField([], $data)', - 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data)', - 'Statamic.$conditions.showField([], $data)', - 'Statamic.$conditions.showField([], $data)', - 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data)', - 'Statamic.$conditions.showField([], $data)', - 'Statamic.$conditions.showField([], $data)', - 'Statamic.$conditions.showField([], $data)', + 'Statamic.$conditions.showField([], $data, \'name\')', + 'Statamic.$conditions.showField([], $data, \'email\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data, \'message\')', + 'Statamic.$conditions.showField([], $data, \'likes_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['name' => 'not empty']]).', $data, \'my_favourites\')', ]; $this->assertStringNotContainsString('Statamic.$conditions', $outputWithJsDisabled); @@ -255,38 +438,97 @@ public function it_renders_show_field_js() } #[Test] - public function it_renders_scoped_show_field_js() + public function it_renders_scoped_show_field_js_inside_legacy_fields_array() { $outputWithJsDisabled = $this->tag('{{ form:contact }}{{ /form:contact }}'); $output = $this->tag(<<<'EOT' {{ form:contact js="alpine:my_form" }} - - {{ fields }} - + {{ field }} {{ /fields }} {{ /form:contact }} EOT ); - preg_match_all('/<\/template>/U', $output, $js); + preg_match_all('//U', $output, $js); $expected = [ - 'Statamic.$conditions.showField([], $data.my_form)', - 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data.my_form)', - 'Statamic.$conditions.showField([], $data.my_form)', - 'Statamic.$conditions.showField([], $data.my_form)', - 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data.my_form)', - 'Statamic.$conditions.showField([], $data.my_form)', - 'Statamic.$conditions.showField([], $data.my_form)', - 'Statamic.$conditions.showField([], $data.my_form)', + 'Statamic.$conditions.showField([], $data.my_form, \'name\')', + 'Statamic.$conditions.showField([], $data.my_form, \'email\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data.my_form, \'message\')', + 'Statamic.$conditions.showField([], $data.my_form, \'likes_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['name' => 'not empty']]).', $data.my_form, \'my_favourites\')', ]; $this->assertStringNotContainsString('Statamic.$conditions', $outputWithJsDisabled); $this->assertEquals($expected, $js[1]); } + #[Test] + public function it_renders_top_level_show_field_js() + { + $outputWithJsDisabled = $this->tag('{{ form:contact }}{{ /form:contact }}'); + + $output = $this->tag(<<<'EOT' +{{ form:contact js="alpine" }} + + + + + + +{{ /form:contact }} +EOT + ); + + preg_match_all('/<\/template>/U', $output, $js); + + $expected = [ + 'Statamic.$conditions.showField([], $data, \'name\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data, \'message\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['name' => 'not empty']]).', $data, \'my_favourites\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['$root.likes_animals' => 'is true']]).', $data, \'my_favourites.favourite_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['favourite_animals' => 'not empty']]).', $data, \'my_favourites.non_favourite_animals\')', + 'Statamic.$conditions.showField([], $data, \'my_favourites.favourite_colour\')', + ]; + + $this->assertStringNotContainsString('Statamic.$conditions', $outputWithJsDisabled); + $this->assertSame($expected, $js[1]); + } + + #[Test] + public function it_renders_scoped_top_level_show_field_js() + { + $outputWithJsDisabled = $this->tag('{{ form:contact }}{{ /form:contact }}'); + + $output = $this->tag(<<<'EOT' +{{ form:contact js="alpine:my_form" }} + + + + + + +{{ /form:contact }} +EOT + ); + + preg_match_all('/<\/template>/U', $output, $js); + + $expected = [ + 'Statamic.$conditions.showField([], $data.my_form, \'name\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['email' => 'not empty']]).', $data.my_form, \'message\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['name' => 'not empty']]).', $data.my_form, \'my_favourites\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['$root.likes_animals' => 'is true']]).', $data.my_form, \'my_favourites.favourite_animals\')', + 'Statamic.$conditions.showField('.$this->jsonEncode(['if' => ['favourite_animals' => 'not empty']]).', $data.my_form, \'my_favourites.non_favourite_animals\')', + 'Statamic.$conditions.showField([], $data.my_form, \'my_favourites.favourite_colour\')', + ]; + + $this->assertStringNotContainsString('Statamic.$conditions', $outputWithJsDisabled); + $this->assertSame($expected, $js[1]); + } + #[Test] public function it_dynamically_renders_text_field_x_model() { @@ -330,13 +572,13 @@ public function it_dynamically_renders_checkboxes_field_x_model() ], ]; - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); } #[Test] @@ -354,13 +596,13 @@ public function it_dynamically_renders_radio_field_x_model() ], ]; - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); - $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml([''], $config, [], ['js' => 'alpine:my_form']); } #[Test] @@ -379,7 +621,7 @@ public function it_dynamically_renders_select_field_x_model() ]; $expected = [ - '', + '', 'Please select...', 'Cat', 'Armadillo', @@ -390,7 +632,7 @@ public function it_dynamically_renders_select_field_x_model() $this->assertFieldRendersHtml($expected, $config, [], ['js' => 'alpine']); $expectedScoped = [ - '', + '', 'Please select...', 'Cat', 'Armadillo', @@ -413,8 +655,92 @@ public function it_dynamically_renders_asset_field_x_model() ], ]; - $this->assertFieldRendersHtml('', $config, [], ['js' => 'alpine']); - $this->assertFieldRendersHtml('', $config, [], ['js' => 'alpine:my_form']); + $this->assertFieldRendersHtml('', $config, [], ['js' => 'alpine']); + $this->assertFieldRendersHtml('', $config, [], ['js' => 'alpine:my_form']); + } + + #[Test] + public function it_dynamically_renders_group_field_without_x_model_but_x_models_fields_within() + { + $config = [ + 'handle' => 'address', + 'field' => [ + 'type' => 'group', + 'fields' => [ + ['handle' => 'street', 'field' => ['type' => 'text']], + ['handle' => 'country', 'field' => ['type' => 'text']], + ], + ], + ]; + + $expected = [ + '', + '', + '', + '', + ]; + + $this->assertFieldRendersHtml($expected, $config, [], ['js' => 'alpine']); + + $expectedScoped = [ + '', + '', + '', + '', + ]; + + $this->assertFieldRendersHtml($expectedScoped, $config, [], ['js' => 'alpine:my_form']); + } + + #[Test] + public function it_recursively_renders_group_field_without_x_model_but_x_models_deeply_nested_fields_within() + { + $config = [ + 'handle' => 'group_one', + 'field' => [ + 'type' => 'group', + 'fields' => [ + [ + 'handle' => 'nested_field', + 'field' => ['type' => 'text'], + ], + [ + 'handle' => 'group_two', + 'field' => [ + 'type' => 'group', + 'fields' => [ + [ + 'handle' => 'deeply_nested_field', + 'field' => ['type' => 'text'], + ], + ], + ], + ], + ], + ], + ]; + + $expected = [ + '', + '', + '', + '', + '', + '', + ]; + + $this->assertFieldRendersHtml($expected, $config, [], ['js' => 'alpine']); + + $expectedScoped = [ + '', + '', + '', + '', + '', + '', + ]; + + $this->assertFieldRendersHtml($expectedScoped, $config, [], ['js' => 'alpine:my_form']); } #[Test] @@ -441,9 +767,13 @@ public function it_merges_any_x_data_passed_to_the_tag() 'name' => null, 'email' => null, 'message' => null, - 'fav_animals' => [], - 'fav_colour' => null, - 'fav_subject' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => null, + 'favourite_subject' => null, + ], 'winnie' => null, 'extra' => 'yes', ], @@ -462,9 +792,13 @@ public function it_merges_any_x_data_passed_to_the_tag() 'name' => null, 'email' => null, 'message' => null, - 'fav_animals' => [], - 'fav_colour' => null, - 'fav_subject' => null, + 'likes_animals' => false, + 'my_favourites' => [ + 'favourite_animals' => [], + 'non_favourite_animals' => [], + 'favourite_colour' => null, + 'favourite_subject' => null, + ], 'winnie' => null, 'extra' => 'no', ], diff --git a/tests/Tags/Form/FormCreateTest.php b/tests/Tags/Form/FormCreateTest.php index 3be2cc95ad..987c5f2c06 100644 --- a/tests/Tags/Form/FormCreateTest.php +++ b/tests/Tags/Form/FormCreateTest.php @@ -54,7 +54,193 @@ public function it_renders_form_with_redirects_to_anchor() } #[Test] - public function it_dynamically_renders_fields_array() + public function it_dynamically_renders_fields() + { + $output = $this->normalizeHtml($this->tag(<<<'EOT' +{{ form:contact }} + {{ form:fields }} + {{ display }}{{ field }} + {{ /form:fields }} +{{ /form:contact }} +EOT + )); + + $this->assertStringContainsString('Full Name', $output); + $this->assertStringContainsString('Email Address', $output); + $this->assertStringContainsString('Message', $output); + + preg_match_all('/(.+)<\/label>/U', $output, $fieldOrder); + + $this->assertEquals(['Full Name', 'Email Address', 'Message'], $fieldOrder[1]); + } + + #[Test] + public function it_dynamically_renders_fields_with_scope_param() + { + $output = $this->normalizeHtml($this->tag(<<<'EOT' +{{ form:contact }} + {{ form:fields scope="field" }} + {{ field:display }}{{ field:field }} + {{ /form:fields }} +{{ /form:contact }} +EOT + )); + + $this->assertStringContainsString('Full Name', $output); + $this->assertStringContainsString('Email Address', $output); + $this->assertStringContainsString('Message', $output); + + preg_match_all('/(.+)<\/label>/U', $output, $fieldOrder); + + $this->assertEquals(['Full Name', 'Email Address', 'Message'], $fieldOrder[1]); + } + + #[Test] + public function it_dynamically_renders_group_fields_recursively() + { + $this->createForm([ + 'tabs' => [ + 'main' => [ + 'sections' => [ + [ + 'display' => 'Section One', + 'instructions' => 'Section One Instructions', + 'fields' => [ + [ + 'handle' => 'group_one', + 'field' => [ + 'type' => 'group', + 'display' => 'Group One', + 'instructions' => 'Group One Instructions', + 'fields' => [ + [ + 'handle' => 'alpha', + 'field' => [ + 'type' => 'text', + ], + ], + [ + 'handle' => 'bravo', + 'field' => [ + 'type' => 'text', + 'display' => 'Bravo', + 'instructions' => 'This field has instructions!', + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], 'survey'); + + $output = $this->normalizeHtml($this->tag(<<<'EOT' +{{ form:survey }} + {{ sections }} + {{ display }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ form:fields }} + {{ display ?: handle }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ field }} + {{ /form:fields }} + + {{ /sections }} + + {{ form:fields }} + {{ display ?: handle }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ field }} + {{ /form:fields }} + +{{ /form:survey }} +EOT + )); + + $this->assertStringContainsString('Section One (Section One Instructions)', $output); + + $this->assertStringContainsString('Group One (Group One Instructions)', $output); + $this->assertStringContainsString('Group One (Group One Instructions)', $output); + $this->assertStringContainsString('group_one.alpha', $output); + $this->assertStringContainsString('group_one.alpha', $output); + $this->assertStringContainsString('Bravo (This field has instructions!)', $output); + $this->assertStringContainsString('Bravo (This field has instructions!)', $output); + } + + #[Test] + public function it_dynamically_renders_group_fields_recursively_with_scope_param() + { + $this->createForm([ + 'tabs' => [ + 'main' => [ + 'sections' => [ + [ + 'display' => 'Section One', + 'instructions' => 'Section One Instructions', + 'fields' => [ + [ + 'handle' => 'group_one', + 'field' => [ + 'type' => 'group', + 'display' => 'Group One', + 'instructions' => 'Group One Instructions', + 'fields' => [ + [ + 'handle' => 'alpha', + 'field' => [ + 'type' => 'text', + ], + ], + [ + 'handle' => 'bravo', + 'field' => [ + 'type' => 'text', + 'display' => 'Bravo', + 'instructions' => 'This field has instructions!', + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], 'survey'); + + $output = $this->normalizeHtml($this->tag(<<<'EOT' +{{ form:survey }} + {{ sections }} + {{ display }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ form:fields scope="field" }} + {{ field:display ?: field:handle }}{{ if field:instructions }} ({{ field:instructions }}){{ /if }} + {{ field:field }} + {{ /form:fields }} + + {{ /sections }} + + {{ form:fields scope="field" }} + {{ field:display ?: field:handle }}{{ if field:instructions }} ({{ field:instructions }}){{ /if }} + {{ field:field }} + {{ /form:fields }} + +{{ /form:survey }} +EOT + )); + + $this->assertStringContainsString('Section One (Section One Instructions)', $output); + + $this->assertStringContainsString('Group One (Group One Instructions)', $output); + $this->assertStringContainsString('Group One (Group One Instructions)', $output); + $this->assertStringContainsString('group_one.alpha', $output); + $this->assertStringContainsString('group_one.alpha', $output); + $this->assertStringContainsString('Bravo (This field has instructions!)', $output); + $this->assertStringContainsString('Bravo (This field has instructions!)', $output); + } + + #[Test] + public function it_dynamically_renders_fields_using_legacy_array() { $output = $this->normalizeHtml($this->tag(<<<'EOT' {{ form:contact }} @@ -75,14 +261,28 @@ public function it_dynamically_renders_fields_array() } #[Test] - public function it_dynamically_renders_with_form_handle() + public function it_dynamically_renders_fields_with_form_handle() + { + foreach (['contact', 'contact-form', 'kontakt_formular'] as $handle) { + $this->createForm(handle: $handle); + $output = $this->normalizeHtml($this->tag('{{ form in="'.$handle.'" }}{{ form:fields }}{{ field }}{{ /form:fields }}{{ /form }}')); + $formSlug = str_replace('_', '-', $handle); + $this->assertStringContainsString('assertStringContainsString('assertStringContainsString('createForm(handle: $handle); $output = $this->normalizeHtml($this->tag('{{ form in="'.$handle.'" }}{{ fields }}{{ field }}{{ /fields }}{{ /form }}')); - $this->assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertFieldRendersHtml([ - '', + '', ], [ 'handle' => 'favourite_animal', 'field' => [ @@ -99,7 +299,7 @@ public function it_dynamically_renders_text_field() ]); $this->assertFieldRendersHtml([ - '', + '', ], [ 'handle' => 'favourite_animal', 'field' => [ @@ -164,11 +364,11 @@ public function it_dynamically_renders_textarea_field() public function it_dynamically_renders_checkboxes_field() { $this->assertFieldRendersHtml([ - 'Cat', + 'Cat', '', - 'Armadillo', + 'Armadillo', '', - 'rat', + 'rat', ], [ 'handle' => 'favourite_animals', 'field' => [ @@ -182,11 +382,11 @@ public function it_dynamically_renders_checkboxes_field() ]); $this->assertFieldRendersHtml([ - 'Cat', + 'Cat', '', - 'Armadillo', + 'Armadillo', '', - 'rat', + 'rat', ], [ 'handle' => 'favourite_animals', 'field' => [ @@ -206,9 +406,9 @@ public function it_dynamically_renders_checkboxes_field() public function it_dynamically_renders_inline_checkboxes_field() { $this->assertFieldRendersHtml([ - 'Cat', - 'Armadillo', - 'rat', + 'Cat', + 'Armadillo', + 'rat', ], [ 'handle' => 'favourite_animals', 'field' => [ @@ -223,9 +423,9 @@ public function it_dynamically_renders_inline_checkboxes_field() ]); $this->assertFieldRendersHtml([ - 'Cat', - 'Armadillo', - 'rat', + 'Cat', + 'Armadillo', + 'rat', ], [ 'handle' => 'favourite_animals', 'field' => [ @@ -246,11 +446,11 @@ public function it_dynamically_renders_inline_checkboxes_field() public function it_dynamically_renders_radio_field() { $this->assertFieldRendersHtml([ - 'Cat', + 'Cat', '', - 'Armadillo', + 'Armadillo', '', - 'rat', + 'rat', ], [ 'handle' => 'favourite_animal', 'field' => [ @@ -264,11 +464,11 @@ public function it_dynamically_renders_radio_field() ]); $this->assertFieldRendersHtml([ - 'Cat', + 'Cat', '', - 'Armadillo', + 'Armadillo', '', - 'rat', + 'rat', ], [ 'handle' => 'favourite_animal', 'field' => [ @@ -288,9 +488,9 @@ public function it_dynamically_renders_radio_field() public function it_dynamically_renders_inline_radio_field() { $this->assertFieldRendersHtml([ - 'Cat', - 'Armadillo', - 'rat', + 'Cat', + 'Armadillo', + 'rat', ], [ 'handle' => 'favourite_animal', 'field' => [ @@ -305,9 +505,9 @@ public function it_dynamically_renders_inline_radio_field() ]); $this->assertFieldRendersHtml([ - 'Cat', - 'Armadillo', - 'rat', + 'Cat', + 'Armadillo', + 'rat', ], [ 'handle' => 'favourite_animal', 'field' => [ @@ -328,7 +528,7 @@ public function it_dynamically_renders_inline_radio_field() public function it_dynamically_renders_select_field() { $this->assertFieldRendersHtml([ - '', + '', 'Please select...', 'Cat', 'Armadillo', @@ -347,7 +547,7 @@ public function it_dynamically_renders_select_field() ]); $this->assertFieldRendersHtml([ - '', + '', 'Please select...', 'Cat', 'Armadillo', @@ -372,7 +572,7 @@ public function it_dynamically_renders_select_field() public function it_dynamically_renders_multiple_select_field() { $this->assertFieldRendersHtml([ - '', + '', 'Cat', 'Armadillo', 'rat', @@ -391,7 +591,7 @@ public function it_dynamically_renders_multiple_select_field() ]); $this->assertFieldRendersHtml([ - '', + '', 'Cat', 'Armadillo', 'rat', @@ -416,7 +616,7 @@ public function it_dynamically_renders_multiple_select_field() public function it_dynamically_renders_asset_field() { $this->assertFieldRendersHtml([ - '', + '', ], [ 'handle' => 'cat_selfie', 'field' => [ @@ -431,7 +631,7 @@ public function it_dynamically_renders_asset_field() public function it_dynamically_renders_multiple_assets_field() { $this->assertFieldRendersHtml([ - '', + '', ], [ 'handle' => 'cat_selfies', 'field' => [ @@ -504,20 +704,30 @@ public function it_dynamically_renders_sections_array() $output = $this->normalizeHtml($this->tag(<<<'EOT' {{ form:survey }} {{ sections }} - {{ if display}}{{ display }} - {{ /if }}{{ if instructions }}{{ instructions }} - {{ /if }}{{ fields | pluck('handle') | join(',') }} + {{ if display }}{{ display }} - {{ /if }}{{ if instructions }}{{ instructions }} - {{ /if }}{{ form:fields }}{{ handle }},{{ /form:fields }} + {{ if display }}{{ display }} - {{ /if }}{{ if instructions }}{{ instructions }} - {{ /if }}{{ fields }}{{ handle }},{{ /fields }} {{ /sections }} - {{ fields | pluck('handle') | join(',') }} + {{ form:fields }}{{ handle }},{{ /form:fields }} + {{ fields }}{{ handle }},{{ /fields }} {{ /form:survey }} EOT )); - $this->assertStringContainsString('One - One Instructions - alpha,bravo', $output); - $this->assertStringContainsString('Two - Two Instructions - charlie,delta', $output); - $this->assertStringContainsString('echo,fox', $output); + // Assert this all works with suggested `{{ form:fields }}` tag + $this->assertStringContainsString('One - One Instructions - alpha,bravo,', $output); + $this->assertStringContainsString('Two - Two Instructions - charlie,delta,', $output); + $this->assertStringContainsString('echo,fox,', $output); + + // Assert this all works with legacy `{{ fields }}` array for backwards compatibility as well + // In reality, there's nothing wrong with this, but the tag supports extra sugar like recursion + $this->assertStringContainsString('One - One Instructions - alpha,bravo,', $output); + $this->assertStringContainsString('Two - Two Instructions - charlie,delta,', $output); + $this->assertStringContainsString('echo,fox,', $output); // Even though the fields are all nested within sections, - // we should still be able to get them via `{{ fields }}` array at top level... - $this->assertStringContainsString('alpha,bravo,charlie,delta,echo,fox', $output); + // we should still be able to get all of them via tag or array at top level... + $this->assertStringContainsString('alpha,bravo,charlie,delta,echo,fox,', $output); + $this->assertStringContainsString('alpha,bravo,charlie,delta,echo,fox,', $output); } #[Test] @@ -544,14 +754,20 @@ public function it_renders_section_instructions_without_cascading_into_field_ins {{ form:survey }} {{ sections }} {{ display }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ form:fields }} + {{ handle }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ /form:fields }} {{ fields }} - {{ handle }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ handle }}{{ if instructions }} ({{ instructions }}){{ /if }} {{ /fields }} {{ /sections }} + {{ form:fields }} + {{ handle }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ /form:fields }} {{ fields }} - {{ handle }}{{ if instructions }} ({{ instructions }}){{ /if }} + {{ handle }}{{ if instructions }} ({{ instructions }}){{ /if }} {{ /fields }} {{ /form:survey }} @@ -560,11 +776,18 @@ public function it_renders_section_instructions_without_cascading_into_field_ins $this->assertStringContainsString('One (One Instructions)', $output); - // Section instructions should NOT cascade down into field instructions... - $this->assertStringContainsString('alpha', $output); - $this->assertStringContainsString('alpha', $output); - $this->assertStringContainsString('bravo (This field has instructions!)', $output); - $this->assertStringContainsString('bravo (This field has instructions!)', $output); + // Section instructions should NOT cascade down into field instructions with suggested `{{ form:fields }}` tag... + $this->assertStringContainsString('alpha', $output); + $this->assertStringContainsString('alpha', $output); + $this->assertStringContainsString('bravo (This field has instructions!)', $output); + $this->assertStringContainsString('bravo (This field has instructions!)', $output); + + // Assert this all works with legacy `{{ fields }}` array for backwards compatibility as well + // In reality, there's nothing wrong with this, but the tag supports extra sugar like recursion + $this->assertStringContainsString('alpha', $output); + $this->assertStringContainsString('alpha', $output); + $this->assertStringContainsString('bravo (This field has instructions!)', $output); + $this->assertStringContainsString('bravo (This field has instructions!)', $output); } #[Test] diff --git a/tests/Tags/Form/FormTestCase.php b/tests/Tags/Form/FormTestCase.php index c6ee6695ac..e204e54e5e 100644 --- a/tests/Tags/Form/FormTestCase.php +++ b/tests/Tags/Form/FormTestCase.php @@ -93,9 +93,7 @@ protected function createForm($blueprintContents = null, $handle = null) protected function assertFieldRendersHtml($expectedHtmlParts, $fieldConfig, $oldData = [], $extraParams = []) { - $randomString = str_shuffle('nobodymesseswiththehoff'); - - $handle = $fieldConfig['handle'].'_'.$randomString; + $handle = str_shuffle('nobodymesseswiththehoff'); $fields = $oldData ? array_merge([['handle' => 'failing_field', 'field' => ['type' => 'text', 'validate' => 'required']]], [$fieldConfig]) @@ -112,7 +110,7 @@ protected function assertFieldRendersHtml($expectedHtmlParts, $fieldConfig, $old $extraParams = $extraParams ? Html::attributes($extraParams) : ''; $output = $this->normalizeHtml( - $this->tag("{{ form:{$handle} {$extraParams}}}{{ fields }}{{ field}}{{ /fields }}{{ /form:{$handle} }}", $oldData) + $this->tag("{{ form:{$handle} {$extraParams}}}{{ form:fields }}{{ field }}{{ /form:fields }}{{ /form:{$handle} }}", $oldData) ); $expected = collect(Arr::wrap($expectedHtmlParts)) diff --git a/tests/Tags/User/PasswordFormTest.php b/tests/Tags/User/PasswordFormTest.php index 35855b1051..35a151c498 100644 --- a/tests/Tags/User/PasswordFormTest.php +++ b/tests/Tags/User/PasswordFormTest.php @@ -74,9 +74,9 @@ public function it_renders_form_with_fields_array() preg_match_all('/.+<\/label>/U', $output, $actual); $expected = [ - 'Current Password', + 'Current Password', 'Password', - 'Password Confirmation', + 'Password Confirmation', ]; $this->assertEquals($expected, $actual[0]); diff --git a/tests/Tags/User/RegisterFormTest.php b/tests/Tags/User/RegisterFormTest.php index f86fcec19f..916e227090 100644 --- a/tests/Tags/User/RegisterFormTest.php +++ b/tests/Tags/User/RegisterFormTest.php @@ -72,7 +72,7 @@ public function it_renders_form_with_fields_array() $expected = [ 'Email Address', 'Password', - 'Password Confirmation', + 'Password Confirmation', 'Name', ]; @@ -98,7 +98,7 @@ public function it_renders_form_with_fields_array_and_custom_blueprint() $expected = [ 'Email Address', 'Password', - 'Password Confirmation', + 'Password Confirmation', 'Full Name', 'Phone Number', 'Over 18 years of age?',