diff --git a/AGENTS.md b/AGENTS.md index 72f070989dbe..12b9fa580e1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,32 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p - **Accessibility**: Use proper accessibility labels and traits - **Localization**: follow best practices from @docs/localization.md +## Build & Test + +**Always check for the Xcode MCP server first.** +If it is connected, use it to build and test — no exceptions. + +If the Xcode MCP fails (e.g. build errors from unrelated targets), fall back to the Fastlane `test` lane: + +```bash +bundle exec fastlane test +bundle exec fastlane test only_testing:TargetName/Class/method +``` + +If Fastlane also fails, fall back to `xcodebuild` directly: + +```bash +xcodebuild \ + -workspace WordPress.xcworkspace \ + -scheme "${SCHEME}" \ + -destination "platform=iOS Simulator,name=${DEVICE}" \ + test \ + | xcbeautify +``` + +Some test targets (e.g. `WordPressDataTests`) have their own scheme and are not part of the main `WordPress` scheme's test plan. +When the `WordPress` scheme build fails due to an unrelated target, try using the target's dedicated scheme instead. + ## Coding Standards - Follow Swift API Design Guidelines - Use strict access control modifiers where possible diff --git a/Modules/Sources/DesignSystem/Foundation/Bundle+DesignSystem.swift b/Modules/Sources/DesignSystem/Foundation/Bundle+DesignSystem.swift new file mode 100644 index 000000000000..1a5d69be629c --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Bundle+DesignSystem.swift @@ -0,0 +1,14 @@ +import Foundation + +extension Bundle { + class var designSystemBundle: Bundle { +#if DEBUG + // Workaround for https://forums.swift.org/t/swift-5-3-swiftpm-resources-in-tests-uses-wrong-bundle-path/37051 + if let testBundlePath = ProcessInfo.processInfo.environment["XCTestBundlePath"], + let bundle = Bundle(path: "\(testBundlePath)/Modules_DesignSystem.bundle") { + return bundle + } +#endif + return Bundle.module + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/IconName.swift b/Modules/Sources/DesignSystem/Foundation/IconName.swift index 8fca1d9258d8..f1e52829de67 100644 --- a/Modules/Sources/DesignSystem/Foundation/IconName.swift +++ b/Modules/Sources/DesignSystem/Foundation/IconName.swift @@ -22,7 +22,7 @@ public enum IconName: String, CaseIterable { public extension UIImage { enum DS { public static func icon(named name: IconName, with configuration: UIImage.Configuration? = nil) -> UIImage? { - return UIImage(named: name.rawValue, in: .module, with: configuration) + return UIImage(named: name.rawValue, in: .designSystemBundle, with: configuration) } } } @@ -30,7 +30,7 @@ public extension UIImage { public extension Image { enum DS { public static func icon(named name: IconName) -> Image { - return Image(name.rawValue, bundle: .module) + return Image(name.rawValue, bundle: .designSystemBundle) } } } diff --git a/Modules/Sources/DesignSystem/Typography/FontManager.swift b/Modules/Sources/DesignSystem/Typography/FontManager.swift index 123021e2d6fc..676e227dee03 100644 --- a/Modules/Sources/DesignSystem/Typography/FontManager.swift +++ b/Modules/Sources/DesignSystem/Typography/FontManager.swift @@ -9,7 +9,7 @@ public enum FontManager { // Makes sure it's performed only once. private static let register: Void = { - let fontURLs = Bundle.module + let fontURLs = Bundle.designSystemBundle .urls(forResourcesWithExtension: "otf", subdirectory: nil) for fontURL in (fontURLs ?? []) { if !CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, nil) { diff --git a/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift b/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift index b74bcb5062e6..095719ba14f8 100644 --- a/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift +++ b/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift @@ -132,7 +132,7 @@ import Reachability /// to set the view values before presenting the No Results View. /// @objc public class func controller() -> NoResultsViewController { - let storyBoard = UIStoryboard(name: "NoResults", bundle: Bundle.module) + let storyBoard = UIStoryboard(name: "NoResults", bundle: Bundle.wordPressUIBundle) let controller = storyBoard.instantiateViewController(withIdentifier: "NoResults") as! NoResultsViewController return controller } diff --git a/Modules/Tests/DesignSystemTests/IconTests.swift b/Modules/Tests/DesignSystemTests/IconTests.swift index d163469ab0c1..9d3fff211ed0 100644 --- a/Modules/Tests/DesignSystemTests/IconTests.swift +++ b/Modules/Tests/DesignSystemTests/IconTests.swift @@ -4,11 +4,6 @@ import SwiftUI final class IconTests: XCTestCase { - // This test will fail if DesignSystem is built as a dynamic library. For some reason, Xcode can't locate - // the library's resource bundle. - // - // DesignSystem will be built as a dynamic library if it's a dependency of a dynamic library, such as - // the WordPressAuthenticator target. func testCanLoadAllIconsAsUIImage() throws { for icon in IconName.allCases { let _ = try XCTUnwrap(UIImage.DS.icon(named: icon)) diff --git a/Modules/Tests/JetpackStatsTests/MockStatsServiceTests.swift b/Modules/Tests/JetpackStatsTests/MockStatsServiceTests.swift index 29c5c99c0e41..4fa319eba2c5 100644 --- a/Modules/Tests/JetpackStatsTests/MockStatsServiceTests.swift +++ b/Modules/Tests/JetpackStatsTests/MockStatsServiceTests.swift @@ -9,7 +9,7 @@ struct MockStatsServiceTests { @Test("getTopListData returns valid data for posts") func testGetTopListDataPosts() async throws { // GIVEN - let service = MockStatsService(timeZone: .current) + let service = MockStatsService(timeZone: .eastern) let dateInterval = calendar.makeDateInterval(for: .today) // WHEN @@ -43,7 +43,7 @@ struct MockStatsServiceTests { @Test("Verify getChartData returns valid data for views metric with today range") func testGetChartDataViewsToday() async throws { // GIVEN - let service = MockStatsService(timeZone: .current) + let service = MockStatsService(timeZone: .eastern) let dateInterval = calendar.makeDateInterval(for: .today) let granularity = dateInterval.preferredGranularity diff --git a/Modules/Tests/WordPressUITests/NoResultsViewControllerTests.swift b/Modules/Tests/WordPressUIUnitTests/NoResultsViewControllerTests.swift similarity index 99% rename from Modules/Tests/WordPressUITests/NoResultsViewControllerTests.swift rename to Modules/Tests/WordPressUIUnitTests/NoResultsViewControllerTests.swift index 5deac3487fe4..427c2174427a 100644 --- a/Modules/Tests/WordPressUITests/NoResultsViewControllerTests.swift +++ b/Modules/Tests/WordPressUIUnitTests/NoResultsViewControllerTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import WordPress +@testable import WordPressUI class NoResultsViewControllerTests: XCTestCase { diff --git a/Rakefile b/Rakefile index 71925e9cbb86..ee86adc9f01e 100644 --- a/Rakefile +++ b/Rakefile @@ -12,16 +12,12 @@ require 'zlib' RUBY_REPO_VERSION = File.read('./.ruby-version').rstrip XCODE_WORKSPACE = 'WordPress.xcworkspace' -XCODE_SCHEME = 'WordPress' -XCODE_CONFIGURATION = 'Debug' EXPECTED_XCODE_VERSION = File.read('.xcode-version').rstrip GUTENBERG_VERSION = 'v1.121.0' PROJECT_DIR = __dir__ abort('Project directory contains one or more spaces – unable to continue.') if PROJECT_DIR.include?(' ') -task default: %w[test] - desc 'Install required dependencies' task dependencies: %w[dependencies:check assets:check dependencies:gutenberg_xcframeworks] @@ -175,36 +171,6 @@ task :mocks do sh "#{File.join(PROJECT_DIR, 'API-Mocks', 'scripts', 'start.sh')} 8282" end -desc "Build #{XCODE_SCHEME}" -task build: [:dependencies] do - xcodebuild(:build) -end - -desc "Profile build #{XCODE_SCHEME}" -task buildprofile: [:dependencies] do - ENV['verbose'] = '1' - xcodebuild(:build, "OTHER_SWIFT_FLAGS='-Xfrontend -debug-time-compilation -Xfrontend -debug-time-expression-type-checking'") -end - -task timed_build: [:clean] do - require 'benchmark' - time = Benchmark.measure do - Rake::Task['build'].invoke - end - puts "CPU Time: #{time.total}" - puts "Wall Time: #{time.real}" -end - -desc 'Run test suite' -task test: [:dependencies] do - xcodebuild(:build, :test) -end - -desc 'Remove any temporary products' -task :clean do - xcodebuild(:clean) -end - desc 'Checks the source for style errors' task :lint do sh 'pushd BuildTools; export SDKROOT=$(xcrun --sdk macosx --show-sdk-path); swift package plugin --allow-writing-to-directory .. --allow-writing-to-package-directory swiftlint --working-directory .. --quiet; popd' @@ -634,23 +600,6 @@ def display_prompt_response? response == 'Y' end -def xcodebuild(*build_cmds) - cmd = 'xcodebuild' - cmd += " -destination 'platform=iOS Simulator,name=iPhone 16'" - cmd += ' -sdk iphonesimulator' - cmd += " -workspace #{XCODE_WORKSPACE}" - cmd += " -scheme #{XCODE_SCHEME}" - cmd += " -configuration #{xcode_configuration}" - cmd += ' ' - cmd += build_cmds.map(&:to_s).join(' ') - cmd += ' | bundle exec xcpretty && exit ${PIPESTATUS[0]}' unless ENV['verbose'] - sh(cmd) -end - -def xcode_configuration - ENV.fetch('XCODE_CONFIGURATION') { XCODE_CONFIGURATION } -end - def command?(command) system("which #{command} > /dev/null 2>&1") end diff --git a/Tests/KeystoneTests/WordPressUnitTests.xctestplan b/Tests/KeystoneTests/WordPressUnitTests.xctestplan index 107323853bad..f61cbd8bffac 100644 --- a/Tests/KeystoneTests/WordPressUnitTests.xctestplan +++ b/Tests/KeystoneTests/WordPressUnitTests.xctestplan @@ -33,58 +33,65 @@ "testTargets" : [ { "target" : { - "containerPath" : "container:..\/Modules", - "identifier" : "WordPressFluxTests", - "name" : "WordPressFluxTests" + "containerPath" : "container:WordPress.xcodeproj", + "identifier" : "4AD953BA2C21451700D0EEFA", + "name" : "WordPressAuthenticatorTests" } }, { "target" : { - "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "E16AB92914D978240047A2E5", - "name" : "WordPressTest" + "containerPath" : "container:..\/Modules", + "identifier" : "DesignSystemTests", + "name" : "DesignSystemTests" } }, { "target" : { "containerPath" : "container:..\/Modules", - "identifier" : "WordPressCoreTests", - "name" : "WordPressCoreTests" + "identifier" : "WordPressSharedTests", + "name" : "WordPressSharedTests" } }, { "target" : { "containerPath" : "container:..\/Modules", - "identifier" : "WordPressSharedTests", - "name" : "WordPressSharedTests" + "identifier" : "JetpackStatsWidgetsCoreTests", + "name" : "JetpackStatsWidgetsCoreTests" } }, { "target" : { - "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "4A8280FC2E5FE9B60037E180", - "name" : "WordPressKitTests" + "containerPath" : "container:..\/Modules", + "identifier" : "WordPressCoreTests", + "name" : "WordPressCoreTests" } }, { "target" : { "containerPath" : "container:..\/Modules", - "identifier" : "JetpackStatsWidgetsCoreTests", - "name" : "JetpackStatsWidgetsCoreTests" + "identifier" : "WordPressUIUnitTests", + "name" : "WordPressUIUnitTests" } }, { "target" : { "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "4AD953BA2C21451700D0EEFA", - "name" : "WordPressAuthenticatorTests" + "identifier" : "E16AB92914D978240047A2E5", + "name" : "WordPressTest" } }, { "target" : { "containerPath" : "container:..\/Modules", - "identifier" : "WordPressUIUnitTests", - "name" : "WordPressUIUnitTests" + "identifier" : "JetpackStatsTests", + "name" : "JetpackStatsTests" + } + }, + { + "target" : { + "containerPath" : "container:..\/Modules", + "identifier" : "AsyncImageKitTests", + "name" : "AsyncImageKitTests" } }, { @@ -97,8 +104,15 @@ { "target" : { "containerPath" : "container:..\/Modules", - "identifier" : "AsyncImageKitTests", - "name" : "AsyncImageKitTests" + "identifier" : "WordPressFluxTests", + "name" : "WordPressFluxTests" + } + }, + { + "target" : { + "containerPath" : "container:WordPress.xcodeproj", + "identifier" : "4A8280FC2E5FE9B60037E180", + "name" : "WordPressKitTests" } } ], diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressShared.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressShared.xcscheme new file mode 100644 index 000000000000..68d022cc1d87 --- /dev/null +++ b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressShared.xcscheme @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fastlane/Fastfile b/fastlane/Fastfile index aff0c757935f..c2872595ffd2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -183,7 +183,7 @@ before_all do |lane| ENV['FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT'] = '120' # Skip these checks/steps for test lane (not needed for testing) - next if lane == :test_without_building + next if %i[test test_without_building].include?(lane) # Ensure we use the latest version of the toolkit check_for_toolkit_updates unless is_ci || ENV['FASTLANE_SKIP_TOOLKIT_UPDATE_CHECK'] diff --git a/fastlane/lanes/build.rb b/fastlane/lanes/build.rb index 87fb9d5e51d8..d547af1d4d68 100644 --- a/fastlane/lanes/build.rb +++ b/fastlane/lanes/build.rb @@ -37,6 +37,37 @@ # Lanes related to Building and Testing the code # platform :ios do + # Runs tests locally without CI prerequisites (env files, signing, etc.) + # + # @option [String] scheme The scheme to test (default: WordPress) + # @option [String] device The Simulator device name + # @option [String] ios_version The deployment target version + # @option [String] only_testing Specific test target/class/method (e.g. WordPressUnitTests/MyClass/testFoo) + # @option [Boolean] clean Whether to clean before building (default: false for incremental builds) + # + # @example Run all WordPress tests: + # bundle exec fastlane test + # @example Run a single test class: + # bundle exec fastlane test only_testing:WordPressUnitTests/MyClass + # @example Test the Jetpack scheme: + # bundle exec fastlane test scheme:Jetpack + # @example Clean build before testing: + # bundle exec fastlane test clean:true + # + desc 'Run tests locally' + lane :test do |scheme: 'WordPress', device: 'iPhone 17', ios_version: nil, only_testing: nil, clean: false| + run_tests( + workspace: WORKSPACE_PATH, + scheme: scheme, + device: device, + derived_data_path: DERIVED_DATA_PATH, + deployment_target_version: ios_version, + only_testing: only_testing, + clean: clean, + skip_package_dependencies_resolution: !clean + ) + end + # Builds the WordPress app for Testing # # @option [String] device the name of the Simulator device to run the tests on