diff --git a/resources/js/components/field-validation/Builder.vue b/resources/js/components/field-validation/Builder.vue index 2128eacc9cf..384f30c8f72 100644 --- a/resources/js/components/field-validation/Builder.vue +++ b/resources/js/components/field-validation/Builder.vue @@ -55,7 +55,7 @@ v-model="customRule" ref="customRuleInput" @keydown.enter.prevent="add(customRule)" - @blur="add(customRule)" + @focusout="add(customRule)" /> this.$refs.rulesSelect.focus()); + this.$nextTick(() => this.$refs.rulesSelect?.focus()); }, add(rule) { @@ -248,7 +248,7 @@ export default { this.resetState(); this.selectedLaravelRule = rule; this.customRule = rule; - this.$nextTick(() => this.$refs.customRuleInput.focus()); + this.$nextTick(() => this.$refs.customRuleInput?.focus()); } else { this.ensure(rule); } diff --git a/resources/js/tests/components/field-validation/Builder.test.js b/resources/js/tests/components/field-validation/Builder.test.js new file mode 100644 index 00000000000..560532b9ad5 --- /dev/null +++ b/resources/js/tests/components/field-validation/Builder.test.js @@ -0,0 +1,50 @@ +import { mount } from '@vue/test-utils'; +import { expect, test, vi } from 'vitest'; +import Builder from '@/components/field-validation/Builder.vue'; + +vi.mock('@inertiajs/vue3', () => ({ + usePage: () => ({ props: { extensionRules: [] } }), +})); + +globalThis.__ = (key) => key; +globalThis.__n = (key) => key; +globalThis.clone = (value) => structuredClone(value); + +const mountBuilder = () => + mount(Builder, { + props: { config: {} }, + global: { + mocks: { + $config: { get: () => '12.0.0' }, + }, + stubs: { + SortableList: true, + }, + }, + }); + +test('a typed rule parameter is committed when the input loses focus', async () => { + const wrapper = mountBuilder(); + + // Picking a parameterized rule swaps the combobox for a plain input holding "after:" + wrapper.vm.add('after:'); + await wrapper.vm.$nextTick(); + + const input = wrapper.find('input'); + await input.setValue('after:{this}.start_time'); + await input.trigger('focusout'); + + expect(wrapper.emitted('updated').at(-1)[0]).toEqual(['after:{this}.start_time']); +}); + +test('an unfinished rule is not committed when the input loses focus', async () => { + const wrapper = mountBuilder(); + + wrapper.vm.add('after:'); + await wrapper.vm.$nextTick(); + + const emitsBefore = wrapper.emitted('updated')?.length ?? 0; + await wrapper.find('input').trigger('focusout'); + + expect(wrapper.emitted('updated')?.length ?? 0).toBe(emitsBefore); +});