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/EditorAssetsLibrary.swift b/ios/Sources/GutenbergKit/Sources/EditorAssetsLibrary.swift index 6028cf5c..a22f0383 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorAssetsLibrary.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorAssetsLibrary.swift @@ -2,6 +2,9 @@ import Foundation import CryptoKit import SwiftSoup +#if canImport(UIKit) +import UIKit + public actor EditorAssetsLibrary { enum ManifestError: Error { case unavailable @@ -166,6 +169,8 @@ private extension String { } } +#endif + struct EditorAssetsMainifest: Codable { var scripts: String var styles: String @@ -220,7 +225,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) } } @@ -243,7 +252,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 881b5f6c..e1ae40ed 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift @@ -1,42 +1,303 @@ 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 = "" - public var webViewGlobals: [WebViewGlobal] = [] + /// 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? + /// Cookies to be sent with WebView-originated requests public var cookies: [HTTPCookie] = [] - public init(title: String = "", content: String = "") { + /// 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, + cookies: cookies + ) + } + + 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? + private var cookies: [HTTPCookie] + + 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, + 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 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 mutating func updateEditorSettings(_ settings: [String: Any]?) { - self.editorSettings = settings + public func setAuthHeader(_ authHeader: String) -> EditorConfigurationBuilder { + var copy = self + copy.authHeader = authHeader + return copy } - public static let `default` = EditorConfiguration() + 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 setCookies(_ cookies: [HTTPCookie]) -> EditorConfigurationBuilder { + var copy = self + copy.cookies = cookies + 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, + cookies: cookies + ) + } } -public struct WebViewGlobal { +public struct WebViewGlobal: Equatable { let name: String let value: WebViewGlobalValue @@ -59,7 +320,7 @@ public enum WebViewGlobalError: Error { case invalidIdentifier(String) } -public enum WebViewGlobalValue { +public enum WebViewGlobalValue: Equatable { case string(String) case number(Double) case boolean(Bool) @@ -76,8 +337,11 @@ public enum WebViewGlobalValue { case .boolean(let bool): return "\(bool)" case .object(let dict): - let pairs = dict.map { key, value in - "\"\(key.escaped)\": \(value.toJavaScript())" + 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): @@ -88,6 +352,8 @@ public enum WebViewGlobalValue { } } +public typealias EditorSettings = [String: Encodable] + // String escaping extension private extension String { var escaped: String { diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 912833bc..afe89905 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 @@ -127,7 +129,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, @@ -149,27 +151,11 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro } private func getEditorConfiguration() -> WKUserScript { - let escapedTitle = configuration.title.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! - let escapedContent = configuration.content.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! - // Generate JavaScript globals let globalsJS = configuration.webViewGlobals.map { global in "window[\"\(global.name)\"] = \(global.value.toJavaScript());" }.joined(separator: "\n") - // 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)") - } - } - let jsCode = """ \(globalsJS) @@ -179,14 +165,14 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro 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)' }, }; @@ -566,3 +552,4 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler { } } } +#endif diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift index d8a87c84..e4ceacc4 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) @@ -35,6 +36,7 @@ public protocol EditorViewControllerDelegate: AnyObject { func editor(_ viewController: EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) } +#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..18d595e1 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/EditorConfigurationBuilderTests.swift @@ -0,0 +1,180 @@ +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) + #expect(builder.cookies == []) + } + + @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/")) + .setCookies([HTTPCookie(properties: [.name: "foo", .value: "bar", .domain: "example.com", .path: "/"])!]) + .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/")) + #expect(configuration.cookies == [HTTPCookie(properties: [.name: "foo", .value: "bar", .domain: "example.com", .path: "/"])!]) + } + + @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/")) + } + + @Test("Sets cookies Correctly") + func editorConfigurationBuilderSetsCookiesCorrectly() throws { + #expect(EditorConfigurationBuilder().setCookies([HTTPCookie(properties: [.name: "foo", .value: "bar", .domain: "example.com", .path: "/"])!]).build().cookies == [HTTPCookie(properties: [.name: "foo", .value: "bar", .domain: "example.com", .path: "/"])!]) + } +} diff --git a/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift b/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift index 29e451f8..eb328fbf 100644 --- a/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift +++ b/ios/Tests/GutenbergKitTests/EditorConfigurationTests.swift @@ -1,11 +1,12 @@ -import XCTest +import Foundation +import Testing @testable import GutenbergKit -final class EditorConfigurationTests: XCTestCase { +@Suite +final class EditorConfigurationTests { - // MARK: - WebViewGlobal Tests - - func testValidJavaScriptIdentifiers() { + @Test + func testValidJavaScriptIdentifiers() throws { let validIdentifiers = [ "myVar", "_privateVar", @@ -17,10 +18,11 @@ final class EditorConfigurationTests: XCTestCase { ] for identifier in validIdentifiers { - XCTAssertNoThrow(try WebViewGlobal(name: identifier, value: .string("test"))) + _ = try WebViewGlobal(name: identifier, value: .string("test")) } } + @Test func testInvalidJavaScriptIdentifiers() { let invalidIdentifiers = [ "123invalid", @@ -33,12 +35,14 @@ final class EditorConfigurationTests: XCTestCase { ] for identifier in invalidIdentifiers { - XCTAssertThrowsError(try WebViewGlobal(name: identifier, value: .string("test"))) + #expect(throws: WebViewGlobalError.self, performing: { + try WebViewGlobal(name: identifier, value: .string("test")) + }) } } // MARK: - WebViewGlobalValue Tests - + @Test func testStringValueConversion() { let testCases = [ ("simple", "\"simple\""), @@ -51,11 +55,11 @@ final class EditorConfigurationTests: XCTestCase { ] for (input, expected) in testCases { - let value = WebViewGlobalValue.string(input) - XCTAssertEqual(value.toJavaScript(), expected) + #expect(WebViewGlobalValue.string(input).toJavaScript() == expected) } } + @Test func testNumberValueConversion() { let testCases = [ (42.0, "42.0"), @@ -65,21 +69,23 @@ final class EditorConfigurationTests: XCTestCase { ] for (input, expected) in testCases { - let value = WebViewGlobalValue.number(input) - XCTAssertEqual(value.toJavaScript(), expected) + #expect(WebViewGlobalValue.number(input).toJavaScript() == expected) } } + @Test func testBooleanValueConversion() { - XCTAssertEqual(WebViewGlobalValue.boolean(true).toJavaScript(), "true") - XCTAssertEqual(WebViewGlobalValue.boolean(false).toJavaScript(), "false") + #expect(WebViewGlobalValue.boolean(true).toJavaScript() == "true") + #expect(WebViewGlobalValue.boolean(false).toJavaScript() == "false") } + @Test func testNullValueConversion() { - XCTAssertEqual(WebViewGlobalValue.null.toJavaScript(), "null") + #expect(WebViewGlobalValue.null.toJavaScript() == "null") } - func testObjectValueConversion() { + @Test + func testObjectValueConversion() throws { let object = WebViewGlobalValue.object([ "name": .string("test"), "count": .number(42), @@ -90,19 +96,11 @@ final class EditorConfigurationTests: XCTestCase { ]) let actual = object.toJavaScript() - let expected = "{\"name\": \"test\",\"active\": true,\"count\": 42.0,\"nested\": {\"value\": \"nested\"}}" - - guard let actualData = actual.data(using: .utf8), - let expectedData = expected.data(using: .utf8), - let actualJSON = try? JSONSerialization.jsonObject(with: actualData) as? [String: Any], - let expectedJSON = try? JSONSerialization.jsonObject(with: expectedData) as? [String: Any] else { - XCTFail("Failed to parse JSON") - return - } - - XCTAssertEqual(actualJSON as NSDictionary, expectedJSON as NSDictionary) + let expected = "{\"active\": true,\"count\": 42.0,\"name\": \"test\",\"nested\": {\"value\": \"nested\"}}" + #expect(actual == expected) } + @Test func testArrayValueConversion() { let array = WebViewGlobalValue.array([ .string("test"), @@ -112,6 +110,6 @@ final class EditorConfigurationTests: XCTestCase { ]) let expected = "[\"test\",42.0,true,null]" - XCTAssertEqual(array.toJavaScript(), expected) + #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) - } -}