Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# AGENTS.md

This file provides guidance to AI agents when working with code in this repository.

## Overview
Expand Down Expand Up @@ -38,6 +36,85 @@ 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

### Prerequisites

- Xcode version in `.xcode-version`, Ruby version in `.ruby-version`
- Run `rake dependencies` once after cloning to install gems and download Gutenberg xcframeworks

### Commands

| Task | Command |
|------|---------|
| Install dependencies | `rake dependencies` |
| Lint | `rake lint` |
| Auto-fix lint issues | `rake lintfix` |

### Building and Testing

Use the `test` Fastlane lane for local testing.
It skips CI prerequisites and reuses `DerivedData/` for incremental builds.

```bash
# Run all tests
bundle exec fastlane test

# Run a specific test target/class/method (fastest option)
bundle exec fastlane test only_testing:WordPressCoreTests/LockingHashMapTests
bundle exec fastlane test only_testing:WordPressTest/SomeClass/testMethod

# Test a different scheme
bundle exec fastlane test scheme:Jetpack

# Clean build before testing
bundle exec fastlane test clean:true
```

Test targets (use with `only_testing`):

- `WordPressTest` — main app unit tests
- `WordPressCoreTests` — core module tests
- `WordPressSharedTests` / `WordPressSharedObjCTests`
- `WordPressKitTests` / `WordPressAuthenticatorTests`
- `WordPressUIUnitTests` / `AsyncImageKitTests`
- `JetpackStatsWidgetsCoreTests` / `JetpackStatsTests`
- `WordPressFluxTests` / `WordPressIntelligenceTests`
- `DesignSystemTests`

For a compile-only check without running tests:

```bash
xcodebuild build \
-workspace WordPress.xcworkspace \
-scheme WordPress \
-destination 'platform=iOS Simulator,name=<device>' \
2>&1 | xcbeautify
```

Pick an available simulator with `xcrun simctl list devices available`.

### Modules

Modules under `Modules/` are an SPM package but target **iOS only** — `swift test` does **not** work from the command line.
Test targets are defined in `Modules/Package.swift` and run via the app's test plans through `xcodebuild`.

When adding a new test target in `Modules/Package.swift`, also add it to the default test plan at `Tests/KeystoneTests/WordPressUnitTests.xctestplan`.
Use `container:../Modules` as the `containerPath` and the SPM target name as the `identifier`.

### SwiftLint

Config: `.swiftlint.yml` (opt-in rules only).
Run via `rake lint`.

## Verifying Changes

After making changes, close the feedback loop:

1. **Lint** — `rake lint` (fast, catches style and localization errors)
2. **Build** — `xcodebuild build ...` (catches type errors without running tests)
3. **Test** — `bundle exec fastlane test only_testing:TargetName/Class/method`

## Coding Standards
- Follow Swift API Design Guidelines
- Use strict access control modifiers where possible
Expand Down
213 changes: 213 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1,214 @@
<<<<<<< HEAD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a merge conflict token.

@AGENTS.md
=======
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

WordPress for iOS is the official mobile app for WordPress that lets users create, manage, and publish content to their WordPress websites directly from their iPhone or iPad.

Minimum requires iOS version is iOS 16.

## High-Level Architecture

### Project Structure
WordPress-iOS uses a modular architecture with the main app and separate Swift packages:

- **Main App**: `WordPress/Classes/` - core app functionality
- **Modules**: `Modules/Sources/` - Reusable Swift packages including:
- `WordPressUI` - shared UI components
- `WordPressFlux` - deprecated state management using Flux pattern (DO NOT USE)
- `WordPressKit` - API client and networking
- `WordPressShared` - Shared utilities
- `DesignSystem` - design system

### Key Patterns
- **Architecture**: SwiftUI with MVVM for new features
- **ViewModels**: Use `@MainActor` class conforming to `ObservableObject` with `@Published` properties
- **Concurrency**: Swift async/await patterns with `@MainActor` for UI thread safety
- **Navigation**: SwiftUI NavigationStack
- **Persistence**: Core Data with `@FetchRequest` for SwiftUI integration
- **UI**: Progressive SwiftUI adoption using `UIHostingController` bridge pattern
- **Dependency Injection**: Constructor injection with protocol-based services

#### Testing Patterns
- Use Swift Testing for new tests

### Important Considerations
- **Multi-site Support**: Code must handle both WordPress.com and self-hosted sites
- **Accessibility**: Use proper accessibility labels and traits
- **Localization**: follow best practices from @docs/localization.md

## Build & Test

### Prerequisites

- Xcode version in `.xcode-version`, Ruby version in `.ruby-version`
- Run `rake dependencies` once after cloning to install gems and download Gutenberg xcframeworks

### Commands

| Task | Command |
|------|---------|
| Install dependencies | `rake dependencies` |
| Lint | `rake lint` |
| Auto-fix lint issues | `rake lintfix` |

### Building and Testing

Use the `test` Fastlane lane for local testing.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might change soon with Xcode 26.3, I haven't had a chance to install the RC yet.

It skips CI prerequisites and reuses `DerivedData/` for incremental builds.

**Do not run multiple `fastlane test` invocations in parallel.**
They share `DerivedData/` and the build database will lock, causing failures.

```bash
# Run all tests
bundle exec fastlane test

# Run a specific test target/class/method (fastest option)
bundle exec fastlane test only_testing:WordPressCoreTests/LockingHashMapTests
bundle exec fastlane test only_testing:WordPressTest/SomeClass/testMethod

# Test a different scheme
bundle exec fastlane test scheme:Jetpack

# Clean build before testing
bundle exec fastlane test clean:true
```

Test targets (use with `only_testing`):

- `WordPressTest` — main app unit tests
- `WordPressCoreTests` — core module tests
- `WordPressSharedTests` / `WordPressSharedObjCTests`
- `WordPressKitTests` / `WordPressAuthenticatorTests`
- `WordPressUIUnitTests` / `AsyncImageKitTests`
- `JetpackStatsWidgetsCoreTests` / `JetpackStatsTests`
- `WordPressFluxTests` / `WordPressIntelligenceTests`
- `DesignSystemTests`

For a compile-only check without running tests:

```bash
xcodebuild build \
-workspace WordPress.xcworkspace \
-scheme WordPress \
-destination 'platform=iOS Simulator,name=<device>' \
2>&1 | xcbeautify
```

Pick an available simulator with `xcrun simctl list devices available`.

### Modules

Modules under `Modules/` are an SPM package but target **iOS only** — `swift test` does **not** work from the command line.
Test targets are defined in `Modules/Package.swift` and run via the app's test plans through `xcodebuild`.

Source → test target mapping follows `{ModuleName}Tests` (e.g., `WordPressCore` → `WordPressCoreTests`).
Exception: `WordPressUI` → `WordPressUIUnitTests` because `WordPressUITests` is taken by the Xcode UI test target.
Not all modules have tests.
Check `Modules/Tests/` to confirm a test target exists before running.

When adding a new test target in `Modules/Package.swift`, also add it to the default test plan at `Tests/KeystoneTests/WordPressUnitTests.xctestplan`.
Use `container:../Modules` as the `containerPath` and the SPM target name as the `identifier`.

### SwiftLint

Config: `.swiftlint.yml` (opt-in rules only).
Run via `rake lint`.

## Verifying Changes

After making changes, close the feedback loop:

1. **Lint** — `rake lint` (fast, catches style and localization errors)
2. **Build** — `xcodebuild build ...` (catches type errors without running tests)
3. **Test** — `bundle exec fastlane test only_testing:TargetName/Class/method`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to be so prescriptive. LLM is already smart enough to know most of these: the xcodebuild build, the simctl command, and the test filter format. They may not know what rake tasks and fastlane lanes we have, so I guess it might be valuable to include them explicitly.

I think most of the time, LLM is smart enough to know which tools to use and how to use them during the job, which usually works out better than telling them what to do (via Claude.md in this case) before the job starts. In general, I'm leaning more towards cutting down the context, rather than adding more.

Please note, I'm not really against adding these instructions here. We can see how they work in practice after this PR is merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like being prescriptive either, but every time I ask the model to generate this kind of stuff, it always opts for being verbose and prescriptive.

I don't like it, because it seems like a maintenance nightmare.

In fact, I had to push back on it enumerating all the module-tests pairs, and force it to track the naming convention instead.

I asked why and the reason it gave, which I know is not to be trusted but made sense to me, is that being prescriptive means it has less files to parse when needing to find the answer to which command to run for what.

## Coding Standards
- Follow Swift API Design Guidelines
- Use strict access control modifiers where possible
- Use four spaces (not tabs)
- Lines should not have trailing whitespace
- Follow the standard formatting practices enforced by SwiftLint
- Don't create `body` for `View` that are too long
- Use semantics text sizes like `.headline`

## Development Workflow
- Branch from `trunk` (main branch)
- PR target should be `trunk`
- When writing commit messages, never include references to Claude

## Release Notes Compilation Process

### Overview
Process for compiling release notes for new iOS versions, maintaining the established tone without App Store character limits.

### Step-by-Step Process

#### 1. Study Previous Release Notes Style
Use `gh` to fetch releases and analyze professional editorialization patterns:
- **Version 25.9**: Raw added in commits `ce08612ecc324e981ef9c5898c98bb267cf29721` & `30cd7073802feb8711b2aae8bb69f41fedba1d80`, editorialized in `bc3af0d2c0c8c3dec8556bb4eff7709f3c151c0d`
- **Version 26.0**: Raw added in commits `8a9e79587924f85e6ac6756fe47d045f7db04ece` & `883acc3324abe45d0e121f3854dc84712b22b4cb`, editorialized in `2ef13c2898c5b58d09c8a3af9f109a47f0bd782c`

Commands: `gh release view 25.9`, `gh release view 26.0` (note: no 'v' prefix)

**Important**: GitHub releases only show WordPress release notes. For better Jetpack release notes, use:
```bash
gh api "repos/wordpress-mobile/WordPress-iOS/contents/WordPress/Jetpack/Resources/release_notes.txt?ref=<commit_hash>" --jq -r '.content' | base64 -d
```

#### 2. Verify Release Branch and Get Last Release Hash
- Verify current branch follows naming: `release/x.y` (where x.y = last_release + 0.1)
- Get commit hash for last release: `gh release view <last_version> --json tagName,targetCommitish`
- Confirm current branch is properly ahead of last release tag

#### 3. Identify Changes Since Last Release
Compare current release branch against last release hash using GitHub API (since local commits may not exist due to squashing/rebasing):
```bash
gh api repos/wordpress-mobile/WordPress-iOS/compare/<last_release_hash>...HEAD --jq '.commits[] | "\(.sha[0:7]) \(.commit.message | split("\n")[0])"'
```
Focus on user-facing changes from squash commit messages. **Important**: When commit messages are unclear or technical, always investigate further:
- Use `gh pr view <PR_number>` to read PR titles and descriptions
- Look for keywords indicating user-facing changes: "feat:", new functionality, UI changes, user experience
- Be especially careful with feature rollouts that may have technical-sounding commit messages but represent new user functionality
- When in doubt, investigate the PR rather than excluding potentially important features

#### 4. Compile Raw Release Notes
Create factual summary including:
- **Always check RELEASE-NOTES.txt file** (note: hyphen, not underscore) for developer-authored release notes under the version number section. These notes start with `[*]`, `[**]`, or `[***]` (stars indicate importance) and **must be included** in the raw release notes
- Only user-facing changes (exclude CI, refactoring, technical debt)
- Prioritize: New features → Improvements → Performance enhancements
- Use positive language (avoid "bug fix", prefer "improved", "enhanced", "resolved")
- Mark changes as WordPress-specific, Jetpack-specific, or both

#### 5. User Confirmation
Present raw notes to user for:
- Accuracy verification
- WordPress vs Jetpack feature classification
- Any missing or incorrect changes
- Approval to proceed with editorialization

#### 6. Editorialization
Transform raw notes using established playful style:
- Use engaging, user-friendly language
- Reference previous release note styles from step 1
- Create separate versions for WordPress and Jetpack apps
- Focus on user benefits and experience improvements

#### 7. Update Release Notes Files
Once user confirms the editorialized release notes, **replace** the contents of the following files (discard any existing content):
- **WordPress release notes**: `WordPress/Resources/release_notes.txt`
- **Jetpack release notes**: `WordPress/Jetpack/Resources/release_notes.txt`

Document any process refinements discovered during execution.

### Content Guidelines
- **Include**: New features, UI improvements, performance enhancements, user experience changes
- **Exclude**: CI changes, code refactoring, dependency updates, internal technical changes
- **Language**: Positive sentiment, avoid "fix" terminology, focus on improvements and enhancements
- **Priority Order**: New features → Improvements → Performance → Other user-facing changes
>>>>>>> c7c40b07a2 (Document test targets and plan convention)
14 changes: 14 additions & 0 deletions Modules/Sources/DesignSystem/Foundation/Bundle+DesignSystem.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 2 additions & 2 deletions Modules/Sources/DesignSystem/Foundation/IconName.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ 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)
}
}
}

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)
}
}
}
2 changes: 1 addition & 1 deletion Modules/Sources/DesignSystem/Typography/FontManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
5 changes: 0 additions & 5 deletions Modules/Tests/DesignSystemTests/IconTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions Modules/Tests/JetpackStatsTests/MockStatsServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import XCTest
@testable import WordPress
@testable import WordPressUI

class NoResultsViewControllerTests: XCTestCase {

Expand Down
Loading