(source of truth for POST) plus * a progressive-enhancement host that the `token-select` runtime component * upgrades into an interactive combobox lookup with a flat removable list. */ class TokenSelectFormHelperTest extends TestCase { /** * @return list */ private function itemsFixture(): array { return [ ['id' => 1, 'key' => 'admin.users.view', 'description' => 'View users'], ['id' => 2, 'key' => 'admin.users.edit', 'description' => 'Edit users'], ['id' => 3, 'key' => 'billing.invoices.read', 'description' => 'Read invoices'], ]; } private function render(callable $call): string { ob_start(); try { $call(); } finally { $output = ob_get_clean(); } return $output === false ? '' : $output; } public function testRendersHiddenSelectWithSelectedOptions(): void { $items = $this->itemsFixture(); $html = $this->render(function () use ($items): void { tokenSelectForm( 'role-permissions', 'permission_ids', 'Assigned permissions', 'Select permissions', $items, [1, 3], false, ['description', 'key'] ); }); self::assertStringContainsString(']*selected/', $html); self::assertMatchesRegularExpression('/value="3"[^>]*selected/', $html); self::assertDoesNotMatchRegularExpression('/value="2"[^>]*selected/', $html); } public function testRendersHostDataAttributeAndCombobox(): void { $html = $this->render(function (): void { tokenSelectForm( 'role-permissions', 'permission_ids', 'Assigned permissions', 'Select permissions', $this->itemsFixture(), [1], false, ['description', 'key'] ); }); self::assertStringContainsString('data-app-component="token-select"', $html); self::assertStringContainsString('data-readonly="0"', $html); self::assertStringContainsString('data-token-select-id="role-permissions"', $html); self::assertStringContainsString('data-token-select-field="permission_ids"', $html); // Combobox input + listbox must be present in non-readonly mode. self::assertStringContainsString('role="combobox"', $html); self::assertStringContainsString('data-token-select-input', $html); self::assertStringContainsString('data-token-select-listbox', $html); self::assertStringContainsString('id="role-permissions-listbox"', $html); self::assertStringContainsString('aria-controls="role-permissions-listbox"', $html); } public function testReadonlyModeHidesInteractiveAffordances(): void { $html = $this->render(function (): void { tokenSelectForm( 'role-permissions', 'permission_ids', 'Assigned permissions', 'Select permissions', $this->itemsFixture(), [1, 2], true, ['description', 'key'] ); }); self::assertStringContainsString('data-readonly="1"', $html); self::assertStringContainsString('app-token-select--readonly', $html); // No combobox input, no clear, no remove in readonly. self::assertStringNotContainsString('data-token-select-input', $html); self::assertStringNotContainsString('data-token-select-clear', $html); self::assertStringNotContainsString('data-token-select-remove', $html); self::assertStringNotContainsString('role="combobox"', $html); } public function testSelectedRowsAreFlatAndAlphabeticallySorted(): void { $items = [ ['id' => 1, 'key' => 'zulu.task', 'description' => 'Zeta action'], ['id' => 2, 'key' => 'alpha.task', 'description' => 'Alpha action'], ['id' => 3, 'key' => 'mike.task', 'description' => 'Mike action'], ]; $html = $this->render(function () use ($items): void { tokenSelectForm( 'role-permissions', 'permission_ids', 'Assigned permissions', 'Select permissions', $items, [1, 2, 3], false, ['description', 'key'] ); }); // Alphabetical by label: Alpha (id=2) < Mike (id=3) < Zeta (id=1). // The `data-token-select-row` attribute only appears on the visible selected rows // (not on the hidden