toContain('.listbox-trigger {')
->toContain('min-width: 0;')
->toContain('overflow: hidden;')
->toContain('.listbox-trigger-label {')
->toContain('text-overflow: ellipsis;')
->toContain('white-space: nowrap;');
});
test('listbox trigger height matches shared inputs', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toMatch('/\.listbox-trigger \{[^}]*height: 2\.25rem;/s')
->toMatch('/\.application-settings-workspace \.listbox-trigger[^}]*height: 2rem;/s');
});
test('listbox component uses shared trigger label truncation', function () {
$html = Blade::render(<<<'BLADE'
BLADE);
expect($html)
->toContain('class="w-full min-w-0"')
->toContain('relative min-w-0')
->toContain('listbox-trigger')
->toContain('listbox-trigger-label')
->toContain(':title="current"')
->not->toContain('class="truncate" x-text="current"');
});
test('listboxes stay in their local Alpine scope by default', function () {
$component = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
$html = Blade::render('');
expect($component)->toContain("'portal' => false")
->and($html)
->toContain('x-data="{')
->toContain('x-ref="panel"')
->not->toContain('x-teleport="body"')
->not->toContain('floatingDropdown(')
->not->toContain('style="position: fixed');
});
test('mobile listbox panels stay anchored to their trigger', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->not->toContain('.listbox-panel:not([style*="position: fixed"])')
->not->toContain('transform: translate(-50%, -50%) !important;');
});
test('portaled listboxes measure content without inheriting the viewport width', function () {
$listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
expect($listbox)
->toContain("panel.style.width = 'max-content';")
->toContain('panel.style.minWidth = `${triggerRect.width}px`;')
->toContain('Math.max(triggerRect.width, panel.offsetWidth)');
});
test('searchable listbox component uses shared trigger label truncation', function () {
$html = Blade::render(<<<'BLADE'
BLADE);
expect($html)
->toContain('class="w-full min-w-0"')
->toContain('relative min-w-0')
->toContain('listbox-trigger-label')
->toContain(':title="current"');
});
test('listbox shows an empty state when it has no options', function () {
$listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
$operations = file_get_contents(resource_path('views/livewire/project/shared/resource-operations.blade.php'));
expect($listbox)
->toContain("'emptyText' => 'No options available.'")
->toContain('x-show="options.length === 0"');
expect($operations)->toContain('No network destinations are available on this server.');
});
test('listbox forwards dynamic disabled state to its trigger', function () {
$listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
$operations = file_get_contents(resource_path('views/livewire/project/shared/resource-operations.blade.php'));
expect($listbox)->toContain("\$attributes->whereStartsWith('x-bind:disabled')");
expect($operations)
->toContain('selectedCloneServer: null')
->toContain('selectedCloneProject: null')
->toContain('selectedCloneEnvironment: null')
->toContain('currentServerId: @js($resource->destination->server->id)')
->toContain('currentDestinationUuid: @js($resource->destination->uuid)')
->toContain("server.id == this.currentServerId ? ' (current)' : ''")
->toContain("destination.uuid == this.currentDestinationUuid ? ' (current)' : ''")
->toContain('selectedCloneServer = null;')
->toContain('placeholder="Choose a server…"')
->toContain("this.selectedCloneServer === null || this.selectedCloneServer === ''")
->toContain("x-bind:disabled=\"selectedCloneServer === null || selectedCloneServer === ''\"")
->toContain('x-model="selectedCloneProject"')
->toContain('x-model="selectedCloneEnvironment"')
->toContain('$wire.cloneTo(selectedCloneDestination)')
->toContain('$wire.cloneTo(@js($resource->destination->uuid), selectedCloneEnvironment)')
->toContain('x-bind:disabled="!selectedMoveProject || availableEnvironments.length === 0"');
});
test('listbox waits for change handlers and prevents overlapping selections', function () {
$listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
expect($listbox)
->toContain('saving: false')
->toContain('async choose(option)')
->toContain('await this.$wire.')
->toContain('if (this.saving || option.disabled) return;')
->toContain("'pointer-events-none opacity-70': saving");
});
test('listbox does not send a second live entangle request when using a change handler', function () {
$listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
expect($listbox)->toContain('@elseif ($live && ! $onChange) @entangle($id).live');
});
test('listbox can preserve its client value across Livewire morphs', function () {
$listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
expect($listbox)
->toContain("'preserveValue' => false")
->toContain('@if ($preserveValue) wire:ignore @endif');
});
test('notification event multiselect truncates long selected summaries', function () {
$html = Blade::render(<<<'BLADE'
BLADE);
expect($html)
->toContain('class="w-full min-w-0"')
->toContain('relative min-w-0')
->toContain('listbox-trigger')
->toContain('listbox-trigger-label')
->toContain('Docker cleanup failure, Disk usage warning, Server unreachable, Server patching')
->toContain('title="Docker cleanup failure, Disk usage warning, Server unreachable, Server patching"')
->toContain('4/4');
});
test('notification event multiselect uses the same panel animation as listboxes', function () {
$html = Blade::render(<<<'BLADE'
BLADE);
expect($html)
->toContain('x-transition:enter="transition ease-out duration-100"')
->toContain('x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"')
->toContain('x-transition:enter-end="opacity-100 translate-y-0 scale-100"')
->toContain('x-transition:leave="transition ease-in duration-75"')
->toContain('x-transition:leave-start="opacity-100 translate-y-0 scale-100"')
->toContain('x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]"');
});