From a6fc55a466b55ce4dacb79a1293cbeaa16ab8363 Mon Sep 17 00:00:00 2001 From: Augusto Souza Date: Thu, 18 Sep 2025 23:25:43 -0300 Subject: [PATCH] Ensure consistent URL path handling across iOS versions --- Source/Bridge/Extensions/URL+Utils.swift | 13 +++++ .../PathConfiguration.swift | 4 +- Tests/Bridge/Extensions/URL+UtilsTests.swift | 47 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 Source/Bridge/Extensions/URL+Utils.swift create mode 100644 Tests/Bridge/Extensions/URL+UtilsTests.swift diff --git a/Source/Bridge/Extensions/URL+Utils.swift b/Source/Bridge/Extensions/URL+Utils.swift new file mode 100644 index 00000000..00e7ae74 --- /dev/null +++ b/Source/Bridge/Extensions/URL+Utils.swift @@ -0,0 +1,13 @@ +import Foundation + +extension URL { + + /// A computed property that returns the URL's path, ensuring a trailing slash is preserved if it exists. + /// + /// This property addresses the behavioral difference between `URL.path` in iOS 15 (which strips trailing slashes) + /// and `URL.path()` in iOS 16+ (which preserves them), providing a single, consistent behavior across all OS versions. + var pathPreservingSlash: String { + let components = URLComponents(string: self.absoluteString) + return components?.path ?? "" + } +} diff --git a/Source/Turbo/Path Configuration/PathConfiguration.swift b/Source/Turbo/Path Configuration/PathConfiguration.swift index e89c41ce..c7213c14 100644 --- a/Source/Turbo/Path Configuration/PathConfiguration.swift +++ b/Source/Turbo/Path Configuration/PathConfiguration.swift @@ -60,9 +60,9 @@ public final class PathConfiguration { /// Returns a merged dictionary containing all the properties that match this URL. public func properties(for url: URL) -> PathProperties { if Hotwire.config.pathConfiguration.matchQueryStrings, let query = url.query { - return properties(for: "\(url.path)?\(query)") + return properties(for: "\(url.pathPreservingSlash)?\(query)") } - return properties(for: url.path) + return properties(for: url.pathPreservingSlash) } /// Returns a merged dictionary containing all the properties diff --git a/Tests/Bridge/Extensions/URL+UtilsTests.swift b/Tests/Bridge/Extensions/URL+UtilsTests.swift new file mode 100644 index 00000000..9b1be6ca --- /dev/null +++ b/Tests/Bridge/Extensions/URL+UtilsTests.swift @@ -0,0 +1,47 @@ +@testable import HotwireNative +import XCTest + +class URL_UtilsTests: XCTestCase { + + func testCompatiblePath_withoutTrailingSlash() { + let url = URL(string: "https://www.example.com/path/to/resource")! + let expectedPath = "/path/to/resource" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Path should not have a trailing slash.") + } + + func testCompatiblePath_withTrailingSlash() { + let url = URL(string: "https://www.example.com/path/to/directory/")! + let expectedPath = "/path/to/directory/" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Path should preserve the trailing slash.") + } + + func testCompatiblePath_rootPathWithSlash() { + let url = URL(string: "https://www.example.com/")! + let expectedPath = "/" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Root path should be a single slash.") + } + + func testCompatiblePath_domainOnly() { + let url = URL(string: "https://www.example.com")! + let expectedPath = "" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Path should be empty for a URL with no path component.") + } + + func testCompatiblePath_withTrailingSlash_andQueryParameters() { + let url = URL(string: "https://www.example.com/path/to/directory/?param1=value1¶m2=value2")! + let expectedPath = "/path/to/directory/" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Path should preserve the trailing slash and ignore query parameters.") + } + + func testCompatiblePath_withoutTrailingSlash_andQueryParameters() { + let url = URL(string: "https://www.example.com/path/to/resource?param1=value1")! + let expectedPath = "/path/to/resource" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Path should not have a trailing slash and ignore query parameters.") + } + + func testCompatiblePath_pathWithMultipleSlashes() { + let url = URL(string: "https://www.example.com/path//to/resource/")! + let expectedPath = "/path//to/resource/" + XCTAssertEqual(url.pathPreservingSlash, expectedPath, "Path should preserve all slashes as they are.") + } +}