diff --git a/Makefile b/Makefile index abe232cd..d76d4cfa 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,9 @@ lint-js: npm-dependencies test-js: npm-dependencies npm run test -- run +lint-swift: + swift package plugin swiftlint + local-android-library: build echo "--- :android: Building Library" ./android/gradlew -p ./android :gutenberg:publishToMavenLocal -exclude-task prepareToPublishToS3 diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index 747107a2..ce9cd273 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -66,20 +66,20 @@ struct ContentView: View { private extension EditorConfiguration { static var template: Self { - var configuration = EditorConfiguration.default - - #warning("1. Update the property values below") + #warning("1. Update the siteURL and authHeader values below") #warning("2. Install the Jetpack plugin to the site") - configuration.siteURL = "https://modify-me.com" - configuration.authHeader = "Insert the Authorization header value here" + let siteUrl: String = "https://modify-me.com" + let authHeader: String = "Insert the Authorization header value here" + let siteApiRoot: String = "\(siteUrl)/wp-json/" - // DO NOT CHANGE the properties below - configuration.siteApiRoot = "\(configuration.siteURL)/wp-json/" - configuration.editorAssetsEndpoint = URL(string: configuration.siteApiRoot)!.appendingPathComponent("wpcom/v2/editor-assets") - // The `plugins: true` is necessary for the editor to use 'remote.html' - configuration.plugins = true + let configuration = EditorConfigurationBuilder() + .setSiteUrl(siteUrl) + .setAuthHeader(authHeader) + .setSiteApiRoot(siteApiRoot) + .setEditorAssetsEndpoint(URL(string: siteApiRoot)!.appendingPathComponent("wpcom/v2/editor-assets")) + .setShouldUsePlugins(true) - return configuration + return configuration.build() } } diff --git a/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift b/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift index f44637b1..9410320b 100644 --- a/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift +++ b/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift @@ -245,7 +245,11 @@ struct EditorAssetsMainifest: Codable { for script in try document.select("script[src]") { if let src = try? script.attr("src") { let link = Self.resolveAssetLink(src, defaultScheme: defaultScheme) + #if canImport(UIKit) let newLink = CachedAssetSchemeHandler.cachedURL(forWebLink: link) ?? link + #else + let newLink = link + #endif try script.attr("src", newLink) } } @@ -268,7 +272,11 @@ struct EditorAssetsMainifest: Codable { for stylesheet in try document.select(#"link[rel="stylesheet"][href]"#) { if let href = try? stylesheet.attr("href") { let link = Self.resolveAssetLink(href, defaultScheme: defaultScheme) + #if canImport(UIKit) let newLink = CachedAssetSchemeHandler.cachedURL(forWebLink: link) ?? link + #else + let newLink = link + #endif try stylesheet.attr("href", newLink) } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorBlockPicker.swift b/ios/Sources/GutenbergKit/Sources/EditorBlockPicker.swift index 9eb38249..0155fccc 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorBlockPicker.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorBlockPicker.swift @@ -1,5 +1,6 @@ import SwiftUI +#if canImport(UIKit) // TODO: Add search // TODO: Group these properly struct EditorBlockPicker: View { @@ -217,3 +218,4 @@ struct EditorBlockPickerSection: Identifiable { let name: String let blockTypes: [EditorBlockType] } +#endif diff --git a/ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift b/ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift index 11a739fc..d66301c3 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift @@ -1,35 +1,357 @@ import Foundation public struct EditorConfiguration { - /// The initial title to initialize the editor with. - public var title = "" - /// The initial content to initialize the editor with. - public var content = "" - - public var postID: Int? - public var postType: String? - public var themeStyles = false - public var plugins = false - public var hideTitle = false - public var siteURL = "" - public var siteApiRoot = "" - public var siteApiNamespace: [String] = [] - public var namespaceExcludedPaths: [String] = [] - public var authHeader = "" + /// Initial title for populating the editor + public let title: String + /// Initial content for populating the editor + public let content: String + + /// ID of the post being edited + public let postID: Int? + /// Type of the post being edited + public let postType: String? + /// Toggles application of theme styles + public let shouldUseThemeStyles: Bool + /// Toggles loading plugin-provided editor assets + public let shouldUsePlugins: Bool + /// Toggles visibility of the title field + public let shouldHideTitle: Bool + /// Root URL for the site + public let siteURL: String + /// Root URL for the site API + public let siteApiRoot: String + /// Namespaces for the site API + public let siteApiNamespace: [String] + /// Paths excluded from API namespacing + public let namespaceExcludedPaths: [String] + /// Authorization header + public let authHeader: String + /// Global variables to be made available to the editor + public let webViewGlobals: [WebViewGlobal] /// Raw block editor settings from the WordPress REST API - public var editorSettings: [String: Any]? - /// The locale to use for translations - public var locale = "en" + public let editorSettings: EditorSettings + /// Locale used for translations + public let locale: String + /// Endpoint for loading editor assets, used when enabling `shouldUsePlugins` public var editorAssetsEndpoint: URL? - public init(title: String = "", content: String = "") { + // Cookies + public let cookies: [HTTPCookie] + + /// Deliberately non-public – consumers should use `EditorConfigurationBuilder` to construct a configuration + init( + title: String, + content: String, + postID: Int?, + postType: String?, + shouldUseThemeStyles: Bool, + shouldUsePlugins: Bool, + shouldHideTitle: Bool, + siteURL: String, + siteApiRoot: String, + siteApiNamespace: [String], + namespaceExcludedPaths: [String], + authHeader: String, + webViewGlobals: [WebViewGlobal], + editorSettings: EditorSettings, + locale: String, + editorAssetsEndpoint: URL? = nil, + cookies: [HTTPCookie] = [] + ) { + self.title = title + self.content = content + self.postID = postID + self.postType = postType + self.shouldUseThemeStyles = shouldUseThemeStyles + self.shouldUsePlugins = shouldUsePlugins + self.shouldHideTitle = shouldHideTitle + self.siteURL = siteURL + self.siteApiRoot = siteApiRoot + self.siteApiNamespace = siteApiNamespace + self.namespaceExcludedPaths = namespaceExcludedPaths + self.authHeader = authHeader + self.webViewGlobals = webViewGlobals + self.editorSettings = editorSettings + self.locale = locale + self.editorAssetsEndpoint = editorAssetsEndpoint + self.cookies = cookies + } + + public func toBuilder() -> EditorConfigurationBuilder { + return EditorConfigurationBuilder( + title: title, + content: content, + postID: postID, + postType: postType, + shouldUseThemeStyles: shouldUseThemeStyles, + shouldUsePlugins: shouldUsePlugins, + shouldHideTitle: shouldHideTitle, + siteURL: siteURL, + siteApiRoot: siteApiRoot, + siteApiNamespace: siteApiNamespace, + namespaceExcludedPaths: namespaceExcludedPaths, + authHeader: authHeader, + webViewGlobals: webViewGlobals, + editorSettings: editorSettings, + locale: locale, + editorAssetsEndpoint: editorAssetsEndpoint + ) + } + + var escapedTitle: String { + title.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! + } + + var escapedContent: String { + content.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! + } + + var editorSettingsJSON: String { + // `editorSettings` values are always `encodable` so this should never fail + let jsonData = try! JSONSerialization.data(withJSONObject: editorSettings, options: []) + return String(data: jsonData, encoding: .utf8) ?? "undefined" + } + + public static let `default` = EditorConfigurationBuilder().build() +} + +public struct EditorConfigurationBuilder { + private var title: String + private var content: String + private var postID: Int? + private var postType: String? + private var shouldUseThemeStyles: Bool + private var shouldUsePlugins: Bool + private var shouldHideTitle: Bool + private var siteURL: String + private var siteApiRoot: String + private var siteApiNamespace: [String] + private var namespaceExcludedPaths: [String] + private var authHeader: String + private var webViewGlobals: [WebViewGlobal] + private var editorSettings: EditorSettings + private var locale: String + private var editorAssetsEndpoint: URL? + + public init( + title: String = "", + content: String = "", + postID: Int? = nil, + postType: String? = nil, + shouldUseThemeStyles: Bool = false, + shouldUsePlugins: Bool = false, + shouldHideTitle: Bool = false, + siteURL: String = "", + siteApiRoot: String = "", + siteApiNamespace: [String] = [], + namespaceExcludedPaths: [String] = [], + authHeader: String = "", + webViewGlobals: [WebViewGlobal] = [], + editorSettings: EditorSettings = [:], + locale: String = "en", + editorAssetsEndpoint: URL? = nil + ){ self.title = title self.content = content + self.postID = postID + self.postType = postType + self.shouldUseThemeStyles = shouldUseThemeStyles + self.shouldUsePlugins = shouldUsePlugins + self.shouldHideTitle = shouldHideTitle + self.siteURL = siteURL + self.siteApiRoot = siteApiRoot + self.siteApiNamespace = siteApiNamespace + self.namespaceExcludedPaths = namespaceExcludedPaths + self.authHeader = authHeader + self.webViewGlobals = webViewGlobals + self.editorSettings = editorSettings + self.locale = locale + self.editorAssetsEndpoint = editorAssetsEndpoint + } + + public func setTitle(_ title: String) -> EditorConfigurationBuilder { + var copy = self + copy.title = title + return copy + } + + public func setContent(_ content: String) -> EditorConfigurationBuilder { + var copy = self + copy.content = content + return copy + } + + public func setPostID(_ postID: Int?) -> EditorConfigurationBuilder { + var copy = self + copy.postID = postID + return copy + } + + public func setPostType(_ postType: String?) -> EditorConfigurationBuilder { + var copy = self + copy.postType = postType + return copy + } + + public func setShouldUseThemeStyles(_ shouldUseThemeStyles: Bool) -> EditorConfigurationBuilder { + var copy = self + copy.shouldUseThemeStyles = shouldUseThemeStyles + return copy + } + + public func setShouldUsePlugins(_ shouldUsePlugins: Bool) -> EditorConfigurationBuilder { + var copy = self + copy.shouldUsePlugins = shouldUsePlugins + return copy + } + + public func setShouldHideTitle(_ shouldHideTitle: Bool) -> EditorConfigurationBuilder { + var copy = self + copy.shouldHideTitle = shouldHideTitle + return copy + } + + public func setSiteUrl(_ siteUrl: String) -> EditorConfigurationBuilder { + var copy = self + copy.siteURL = siteUrl + return copy + } + + public func setSiteApiRoot(_ siteApiRoot: String) -> EditorConfigurationBuilder { + var copy = self + copy.siteApiRoot = siteApiRoot + return copy + } + + public func setSiteApiNamespace(_ siteApiNamespace: [String]) -> EditorConfigurationBuilder { + var copy = self + copy.siteApiNamespace = siteApiNamespace + return copy + } + + public func setNamespaceExcludedPaths(_ namespaceExcludedPaths: [String]) -> EditorConfigurationBuilder { + var copy = self + copy.namespaceExcludedPaths = namespaceExcludedPaths + return copy + } + + public func setAuthHeader(_ authHeader: String) -> EditorConfigurationBuilder { + var copy = self + copy.authHeader = authHeader + return copy + } + + public func setWebViewGlobals(_ webViewGlobals: [WebViewGlobal]) -> EditorConfigurationBuilder { + var copy = self + copy.webViewGlobals = webViewGlobals + return copy + } + + public func setEditorSettings(_ editorSettings: EditorSettings) -> EditorConfigurationBuilder { + var copy = self + copy.editorSettings = editorSettings + return copy + } + + public func setLocale(_ locale: String) -> EditorConfigurationBuilder { + var copy = self + copy.locale = locale + return copy + } + + public func setEditorAssetsEndpoint(_ editorAssetsEndpoint: URL?) -> EditorConfigurationBuilder { + var copy = self + copy.editorAssetsEndpoint = editorAssetsEndpoint + return copy + } + + public func build() -> EditorConfiguration { + EditorConfiguration( + title: title, + content: content, + postID: postID, + postType: postType, + shouldUseThemeStyles: shouldUseThemeStyles, + shouldUsePlugins: shouldUsePlugins, + shouldHideTitle: shouldHideTitle, + siteURL: siteURL, + siteApiRoot: siteApiRoot, + siteApiNamespace: siteApiNamespace, + namespaceExcludedPaths: namespaceExcludedPaths, + authHeader: authHeader, + webViewGlobals: webViewGlobals, + editorSettings: editorSettings, + locale: locale, + editorAssetsEndpoint: editorAssetsEndpoint + ) + } +} + +public struct WebViewGlobal: Equatable { + let name: String + let value: WebViewGlobalValue + + public init(name: String, value: WebViewGlobalValue) throws { + // Validate name is a valid JavaScript identifier + guard Self.isValidJavaScriptIdentifier(name) else { + throw WebViewGlobalError.invalidIdentifier(name) + } + self.name = name + self.value = value + } + + private static func isValidJavaScriptIdentifier(_ name: String) -> Bool { + // Add validation logic for JavaScript identifiers + return name.range(of: "^[a-zA-Z_$][a-zA-Z0-9_$]*$", options: .regularExpression) != nil } +} + +public enum WebViewGlobalError: Error { + case invalidIdentifier(String) +} - public mutating func updateEditorSettings(_ settings: [String: Any]?) { - self.editorSettings = settings +public enum WebViewGlobalValue: Equatable { + case string(String) + case number(Double) + case boolean(Bool) + case object([String: WebViewGlobalValue]) + case array([WebViewGlobalValue]) + case null + + func toJavaScript() -> String { + switch self { + case .string(let str): + return "\"\(str.escaped)\"" + case .number(let num): + return "\(num)" + case .boolean(let bool): + return "\(bool)" + case .object(let dict): + let sortedKeys = dict.keys.sorted() + var pairs: [String] = [] + for key in sortedKeys { + let value = dict[key]! + pairs.append("\"\(key.escaped)\": \(value.toJavaScript())") + } + return "{\(pairs.joined(separator: ","))}" + case .array(let array): + return "[\(array.map { $0.toJavaScript() }.joined(separator: ","))]" + case .null: + return "null" + } } +} - public static let `default` = EditorConfiguration() +public typealias EditorSettings = [String: Encodable] + +// String escaping extension +private extension String { + var escaped: String { + return self.replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\t", with: "\\t") + .replacingOccurrences(of: "\u{8}", with: "\\b") + .replacingOccurrences(of: "\u{12}", with: "\\f") + } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 99c17876..e22b4dfe 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -1,9 +1,11 @@ -import UIKit @preconcurrency import WebKit import SwiftUI import Combine import CryptoKit +#if canImport(UIKit) +import UIKit + @MainActor public final class EditorViewController: UIViewController, GutenbergEditorControllerDelegate { public let webView: WKWebView @@ -121,7 +123,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro } private func loadEditor() { - if configuration.plugins { + if configuration.shouldUsePlugins { webView.configuration.userContentController.addScriptMessageHandler( EditorAssetsProvider(library: assetsLibrary), contentWorld: .page, @@ -143,37 +145,29 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro } private func getEditorConfiguration() -> WKUserScript { - let escapedTitle = configuration.title.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! - let escapedContent = configuration.content.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! - - // Convert editor settings to JSON string if available - var editorSettingsJS = "undefined" - if let settings = configuration.editorSettings { - do { - let jsonData = try JSONSerialization.data(withJSONObject: settings, options: []) - if let jsonString = String(data: jsonData, encoding: .utf8) { - editorSettingsJS = jsonString - } - } catch { - NSLog("Failed to serialize editor settings: \(error)") - } - } + + // Generate JavaScript globals + let globalsJS = configuration.webViewGlobals.map { global in + "window[\"\(global.name)\"] = \(global.value.toJavaScript());" + }.joined(separator: "\n") let jsCode = """ + \(globalsJS) + window.GBKit = { siteURL: '\(configuration.siteURL)', siteApiRoot: '\(configuration.siteApiRoot)', siteApiNamespace: \(Array(configuration.siteApiNamespace)), namespaceExcludedPaths: \(Array(configuration.namespaceExcludedPaths)), authHeader: '\(configuration.authHeader)', - themeStyles: \(configuration.themeStyles), - hideTitle: \(configuration.hideTitle), - editorSettings: \(editorSettingsJS), + themeStyles: \(configuration.shouldUseThemeStyles), + hideTitle: \(configuration.shouldHideTitle), + editorSettings: \(configuration.editorSettingsJSON), locale: '\(configuration.locale)', post: { id: \(configuration.postID ?? -1), - title: '\(escapedTitle)', - content: '\(escapedContent)' + title: '\(configuration.escapedTitle)', + content: '\(configuration.escapedContent)' }, }; @@ -423,3 +417,6 @@ private final class GutenbergEditorController: NSObject, WKNavigationDelegate, W } } } + + +#endif diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift index 89b30704..c3fe9be7 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift @@ -1,5 +1,6 @@ import Foundation +#if canImport(UIKit) public protocol EditorViewControllerDelegate: AnyObject { /// Called when the editor loads. func editorDidLoad(_ viewContoller: EditorViewController) @@ -40,6 +41,7 @@ public protocol EditorViewControllerDelegate: AnyObject { /// - parameter type: The type of autocompleter that was triggered (e.g., "plus-symbol", "at-symbol"). func editor(_ viewController: EditorViewController, didTriggerAutocompleter type: String) } +#endif public struct EditorState { /// Set to `true` if the editor has non-empty content. diff --git a/ios/Sources/GutenbergKit/Sources/GBWebView.swift b/ios/Sources/GutenbergKit/Sources/GBWebView.swift index 2f13ae87..7c036fe8 100644 --- a/ios/Sources/GutenbergKit/Sources/GBWebView.swift +++ b/ios/Sources/GutenbergKit/Sources/GBWebView.swift @@ -2,9 +2,11 @@ import WebKit class GBWebView: WKWebView { + #if canImport(UIKit) /// Disables the default bottom bar that competes with the Gutenberg inserter /// override var inputAccessoryView: UIView? { nil } + #endif } diff --git a/ios/Tests/GutenbergKitTests/EditorConfigurationBuilderTests.swift b/ios/Tests/GutenbergKitTests/EditorConfigurationBuilderTests.swift new file mode 100644 index 00000000..7dd3c071 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/EditorConfigurationBuilderTests.swift @@ -0,0 +1,172 @@ +import Foundation +import Testing +@testable import GutenbergKit + +@Suite("Editor Configuration Builder Tests") +struct EditorConfigurationBuilderTests { + + @Test("Editor Configuration Defaults Are Correct") + func testThatEditorConfigurationBuilderDefaultsAreCorrect() throws { + let builder = EditorConfigurationBuilder().build() + #expect(builder.title == "") + #expect(builder.content == "") + #expect(builder.postID == nil) + #expect(builder.postType == nil) + #expect(builder.shouldUseThemeStyles == false) + #expect(builder.shouldUsePlugins == false) + #expect(builder.shouldHideTitle == false) + #expect(builder.siteURL == "") + #expect(builder.siteApiRoot == "") + #expect(builder.siteApiNamespace == []) + #expect(builder.namespaceExcludedPaths == []) + #expect(builder.authHeader == "") + #expect(builder.webViewGlobals == []) + #expect(builder.editorSettings.isEmpty) + #expect(builder.locale == "en") + #expect(builder.editorAssetsEndpoint == nil) + } + + @Test("Editor Configuration to Builder") + func testThatEditorConfigurationToBuilder() throws { + let configuration = try EditorConfigurationBuilder() + .setTitle("Title") + .setContent("Content") + .setPostID(123) + .setPostType("Post") + .setShouldUseThemeStyles(true) + .setShouldUsePlugins(true) + .setShouldHideTitle(true) + .setSiteUrl("https://example.com") + .setSiteApiRoot("/wp-json") + .setSiteApiNamespace(["wp", "v2"]) + .setNamespaceExcludedPaths(["jetpack"]) + .setAuthHeader("Bearer Token") + .setWebViewGlobals([WebViewGlobal(name: "foo", value: .string("bar"))]) + .setEditorSettings(["foo":"bar"]) + .setLocale("fr") + .setEditorAssetsEndpoint(URL(string: "https://example.com/wp-content/plugins/gutenberg/build/")) + .build() // Convert to a configuration + .toBuilder() // Then back to a builder (to test the configuration->builder logic) + .build() // Then back to a configuration to examine the results + + #expect(configuration.title == "Title") + #expect(configuration.content == "Content") + #expect(configuration.postID == 123) + #expect(configuration.postType == "Post") + #expect(configuration.shouldUseThemeStyles == true) + #expect(configuration.shouldUsePlugins == true) + #expect(configuration.shouldHideTitle == true) + #expect(configuration.siteURL == "https://example.com") + #expect(configuration.siteApiRoot == "/wp-json") + #expect(configuration.siteApiNamespace == ["wp", "v2"]) + #expect(configuration.namespaceExcludedPaths == ["jetpack"]) + #expect(configuration.authHeader == "Bearer Token") + #expect(configuration.webViewGlobals == [try WebViewGlobal(name: "foo", value: .string("bar"))]) + #expect(configuration.editorSettingsJSON == #"{"foo":"bar"}"#) + #expect(configuration.locale == "fr") + #expect(configuration.editorAssetsEndpoint == URL(string: "https://example.com/wp-content/plugins/gutenberg/build/")) + } + + @Test("Sets Title Correctly") + func editorConfigurationBuilderSetsTitleCorrectly() throws { + #expect(EditorConfigurationBuilder().setTitle("Title").build().title == "Title") + } + + @Test("Sets Content Correctly") + func editorConfigurationBuilderSetsContentCorrectly() throws { + #expect(EditorConfigurationBuilder().setContent("Content").build().content == "Content") + } + + @Test("Sets PostID Correctly") + func editorConfigurationBuilderSetsPostIDCorrectly() throws { + #expect(EditorConfigurationBuilder().setPostID(nil).build().postID == nil) + #expect(EditorConfigurationBuilder().setPostID(123).build().postID == 123) + } + + @Test("Sets Post Type Correctly") + func editorConfigurationBuilderSetsPostTypeCorrectly() throws { + #expect(EditorConfigurationBuilder().setPostType(nil).build().postType == nil) + #expect(EditorConfigurationBuilder().setPostType("post").build().postType == "post") + } + + @Test("Sets shouldUseThemeStyles Correctly") + func editorConfigurationBuilderSetsShouldUseThemeStylesCorrectly() throws { + #expect(EditorConfigurationBuilder().setShouldUseThemeStyles(true).build().shouldUseThemeStyles) + #expect(!EditorConfigurationBuilder().setShouldUseThemeStyles(false).build().shouldUseThemeStyles) + } + + @Test("Sets shouldUsePlugins Correctly") + func editorConfigurationBuilderSetsShouldUsePluginsCorrectly() throws { + #expect(EditorConfigurationBuilder().setShouldUsePlugins(true).build().shouldUsePlugins) + #expect(!EditorConfigurationBuilder().setShouldUsePlugins(false).build().shouldUsePlugins) + } + + @Test("Sets shouldHideTitle Correctly") + func editorConfigurationBuilderSetsShouldHideTitleCorrectly() throws { + #expect(EditorConfigurationBuilder().setShouldHideTitle(true).build().shouldHideTitle) + #expect(!EditorConfigurationBuilder().setShouldHideTitle(false).build().shouldHideTitle) + } + + @Test("Sets siteUrl Correctly") + func editorConfigurationBuilderSetsSiteUrlCorrectly() throws { + #expect(EditorConfigurationBuilder().setSiteUrl("https://example.com").build().siteURL == "https://example.com") + } + + @Test("Sets siteApiRoot Correctly") + func editorConfigurationBuilderSetsSiteApiRootCorrectly() throws { + #expect(EditorConfigurationBuilder().setSiteApiRoot("https://example.com/wp-json").build().siteApiRoot == "https://example.com/wp-json") + } + + @Test("Sets siteApiNamespace Correctly") + func editorConfigurationBuilderSetsApiNamespaceCorrectly() throws { + #expect(EditorConfigurationBuilder().setSiteApiNamespace(["wp/v2"]).build().siteApiNamespace == ["wp/v2"]) + } + + @Test("Sets namespaceExcludedPaths Correctly") + func editorConfigurationBuilderSetsNamespaceExcludedPathsCorrectly() throws { + #expect( + EditorConfigurationBuilder() + .setNamespaceExcludedPaths(["/wp-admin", "/wp-login.php"]) + .build() + .namespaceExcludedPaths + == ["/wp-admin", "/wp-login.php"] + ) + } + + @Test("Sets authHeader Correctly") + func editorConfigurationBuilderSetsAuthHeaderCorrectly() throws { + #expect(EditorConfigurationBuilder().setAuthHeader("Bearer token").build().authHeader == "Bearer token") + } + + @Test("Sets webViewGlobals Correctly") + func editorConfigurationBuilderSetsWebViewGlobalsCorrectly() throws { + #expect( + try EditorConfigurationBuilder() + .setWebViewGlobals([WebViewGlobal(name: "foo", value: .string("bar"))]) + .build() + .webViewGlobals + == [WebViewGlobal(name: "foo", value: .string("bar"))] + ) + } + + @Test("Sets editorSettings Correctly") + func editorConfigurationBuilderSetsEditorSettingsCorrectly() throws { + #expect( + EditorConfigurationBuilder() + .setEditorSettings(["foo": "bar"]) + .build() + .editorSettingsJSON + == "{\"foo\":\"bar\"}" + ) + } + + @Test("Sets locale Correctly") + func editorConfigurationBuilderSetsLocaleCorrectly() throws { + #expect(EditorConfigurationBuilder().setLocale("en").build().locale == "en") + } + + @Test("Sets editorAssetsEndpoint Correctly") + func editorConfigurationBuilderSetsEditorAssetsEndpointCorrectly() throws { + #expect(EditorConfigurationBuilder().setEditorAssetsEndpoint(URL(string: "https://example.com/wp-content/plugins/gutenberg/build/")).build().editorAssetsEndpoint == URL(string: "https://example.com/wp-content/plugins/gutenberg/build/")) + } +} diff --git a/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift b/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift new file mode 100644 index 00000000..eb328fbf --- /dev/null +++ b/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift @@ -0,0 +1,115 @@ +import Foundation +import Testing +@testable import GutenbergKit + +@Suite +final class EditorConfigurationTests { + + @Test + func testValidJavaScriptIdentifiers() throws { + let validIdentifiers = [ + "myVar", + "_privateVar", + "$jQuery", + "myVar123", + "MY_CONSTANT", + "a", + "A" + ] + + for identifier in validIdentifiers { + _ = try WebViewGlobal(name: identifier, value: .string("test")) + } + } + + @Test + func testInvalidJavaScriptIdentifiers() { + let invalidIdentifiers = [ + "123invalid", + "my-var", + "my.var", + "my var", + "", + "my@var", + "my#var" + ] + + for identifier in invalidIdentifiers { + #expect(throws: WebViewGlobalError.self, performing: { + try WebViewGlobal(name: identifier, value: .string("test")) + }) + } + } + + // MARK: - WebViewGlobalValue Tests + @Test + func testStringValueConversion() { + let testCases = [ + ("simple", "\"simple\""), + ("with \"quotes\"", "\"with \\\"quotes\\\"\""), + ("with\nnewline", "\"with\\nnewline\""), + ("with\ttab", "\"with\\ttab\""), + ("with\rreturn", "\"with\\rreturn\""), + ("with\u{8}backspace", "\"with\\bbackspace\""), + ("with\u{12}formfeed", "\"with\\fformfeed\"") + ] + + for (input, expected) in testCases { + #expect(WebViewGlobalValue.string(input).toJavaScript() == expected) + } + } + + @Test + func testNumberValueConversion() { + let testCases = [ + (42.0, "42.0"), + (-3.14, "-3.14"), + (0.0, "0.0"), + (1.0, "1.0") + ] + + for (input, expected) in testCases { + #expect(WebViewGlobalValue.number(input).toJavaScript() == expected) + } + } + + @Test + func testBooleanValueConversion() { + #expect(WebViewGlobalValue.boolean(true).toJavaScript() == "true") + #expect(WebViewGlobalValue.boolean(false).toJavaScript() == "false") + } + + @Test + func testNullValueConversion() { + #expect(WebViewGlobalValue.null.toJavaScript() == "null") + } + + @Test + func testObjectValueConversion() throws { + let object = WebViewGlobalValue.object([ + "name": .string("test"), + "count": .number(42), + "active": .boolean(true), + "nested": .object([ + "value": .string("nested") + ]) + ]) + + let actual = object.toJavaScript() + let expected = "{\"active\": true,\"count\": 42.0,\"name\": \"test\",\"nested\": {\"value\": \"nested\"}}" + #expect(actual == expected) + } + + @Test + func testArrayValueConversion() { + let array = WebViewGlobalValue.array([ + .string("test"), + .number(42), + .boolean(true), + .null + ]) + + let expected = "[\"test\",42.0,true,null]" + #expect(array.toJavaScript() == expected) + } +} diff --git a/ios/Tests/GutenbergKitTests/EditorManifestTests.swift b/ios/Tests/GutenbergKitTests/EditorManifestTests.swift index 0dfc1402..5a74014d 100644 --- a/ios/Tests/GutenbergKitTests/EditorManifestTests.swift +++ b/ios/Tests/GutenbergKitTests/EditorManifestTests.swift @@ -30,9 +30,15 @@ struct EditorManifestTests { #expect(link.hasPrefix("http://")) } + #if canImport(UIKit) for link in try forEditor.parseAssetLinks(defaultScheme: nil) { #expect(link.hasPrefix("gbk-cache-http://")) } + #else + for link in try forEditor.parseAssetLinks(defaultScheme: nil) { + #expect(link.hasPrefix("http://")) + } + #endif } @Test diff --git a/ios/Tests/GutenbergKitTests/GutenbergKitTests.swift b/ios/Tests/GutenbergKitTests/GutenbergKitTests.swift deleted file mode 100644 index 7057df12..00000000 --- a/ios/Tests/GutenbergKitTests/GutenbergKitTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest -@testable import GutenbergKit - -final class GutenbergKitTests: XCTestCase { - func testExample() throws { - XCTAssert(true) - } -}