From a337c469ccef6d0945afea0ff1636c02a5527717 Mon Sep 17 00:00:00 2001 From: David Hunt Date: Fri, 21 Aug 2026 16:29:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(FOSTestingUI):=20setUp(bundles:)=20?= =?UTF-8?q?=E2=80=94=20multi-bundle=20YAML=20for=20view-test=20harnesses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ViewModelDisplayTestCase/ViewModelViewTestCase can now localize from several bundles merged into one store; the single-bundle setUp delegates to the new form. DocC on both overloads carries the no-YAML/key-echo contract. Skill docs (ui-tests-generator, api-catalog) name both forms; plugin 2.30.0. Also: Localizable.text is now @MainActor. --- .claude-plugin/plugin.json | 2 +- .../fosmvvm-ui-tests-generator/SKILL.md | 1 + .../fosmvvm-ui-tests-generator/reference.md | 4 ++ .../skills/shared/api-catalog/FOSTesting.md | 4 +- CHANGELOG.md | 13 +++++ .../SwiftUI Support/LocalizableViews.swift | 2 +- .../FOSTestingUI/ViewModelViewTestCase.swift | 54 ++++++++++++++++++- 7 files changed, 76 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 70f39ee..f08fda7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "fosmvvm-generators", "description": "FOSMVVM architecture generators for ViewModels, Fields, DataModels, ServerRequests, Leaf Views, and ViewModel Tests", - "version": "2.29.0", + "version": "2.30.0", "author": { "name": "FOS Computer Services" }, diff --git a/.claude/skills/fosmvvm-ui-tests-generator/SKILL.md b/.claude/skills/fosmvvm-ui-tests-generator/SKILL.md index 196b3b2..8688c3c 100644 --- a/.claude/skills/fosmvvm-ui-tests-generator/SKILL.md +++ b/.claude/skills/fosmvvm-ui-tests-generator/SKILL.md @@ -263,6 +263,7 @@ class MyAppViewModelViewTestCase: **Key points:** - Display-only base has **one** generic parameter — `VM`. No stub Operations type. - Interactive base has **two** — `VM` and `VMO`. The `viewModelOperations()` helper is available only on this path. +- Both `setUp` forms exist: `bundle:` for the common single-bundle harness, and `bundles: [Bundle]` when the YAML lives in more than one bundle (the harness's own plus another target's resources) — the localizations merge into one store. Untranslated keys resolve to key-derived placeholder text (never assert on it; localization completeness is `LocalizableTestCase`'s job), so view tests stay drivable with partial or no YAML. - Both wrap FOSTestingUI's `presentView()` and pin the bundle / bundle identifier. - `continueAfterFailure = false` stops tests immediately on failure. diff --git a/.claude/skills/fosmvvm-ui-tests-generator/reference.md b/.claude/skills/fosmvvm-ui-tests-generator/reference.md index fb6606a..a1c7c87 100644 --- a/.claude/skills/fosmvvm-ui-tests-generator/reference.md +++ b/.claude/skills/fosmvvm-ui-tests-generator/reference.md @@ -63,6 +63,10 @@ class {ProjectName}ViewModelViewTestCase Reach for this when: XCUITest-driving a ViewModelView that only displays data — no operations to verify. Create one project-level subclass that pins -`setUp(bundle:resourceDirectoryName:appBundleIdentifier:locales:)`; each test +`setUp(bundle:resourceDirectoryName:appBundleIdentifier:locales:)` — or its +`setUp(bundles:)` twin when the YAML lives in several bundles (the test +harness's own plus another target's resources), merged into one store; each test then calls `presentView()` with a stub ViewModel (localized for you; the suite's `localizationStore` and locale shorthands are available) and asserts on the returned XCUIApplication. `presentView(testConfiguration:)` names a diff --git a/CHANGELOG.md b/CHANGELOG.md index 06027ec..1733064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`setUp(bundles:resourceDirectoryName:appBundleIdentifier:locales:)`** (FOSTestingUI) — + `ViewModelDisplayTestCase` / `ViewModelViewTestCase` harnesses can now localize from + several bundles merged into one store (the harness's own YAML plus another target's + resources); the existing single-`bundle:` form delegates to it. + +### Changed + +- **`Localizable.text` is `@MainActor`** (FOSMVVM) — the SwiftUI resolver view it + constructs belongs on the main actor; call sites in SwiftUI `body` contexts are + unaffected. + ## [0.13.1] - 2026-08-21 ### Added diff --git a/Sources/FOSMVVM/SwiftUI Support/LocalizableViews.swift b/Sources/FOSMVVM/SwiftUI Support/LocalizableViews.swift index 03ebbbc..db06f9f 100644 --- a/Sources/FOSMVVM/SwiftUI Support/LocalizableViews.swift +++ b/Sources/FOSMVVM/SwiftUI Support/LocalizableViews.swift @@ -124,7 +124,7 @@ public extension Localizable { /// ```swift /// viewModel.pageTitle.text /// ``` - var text: some View { + @MainActor var text: some View { LocalizableResolverView(localizable: self) } } diff --git a/Sources/FOSTestingUI/ViewModelViewTestCase.swift b/Sources/FOSTestingUI/ViewModelViewTestCase.swift index acf6ae2..fd1d947 100644 --- a/Sources/FOSTestingUI/ViewModelViewTestCase.swift +++ b/Sources/FOSTestingUI/ViewModelViewTestCase.swift @@ -212,12 +212,64 @@ import XCTest resourceDirectoryName: String = "", appBundleIdentifier: String, locales: Set? = nil + ) async throws { + try await setUp( + bundles: [bundle], + resourceDirectoryName: resourceDirectoryName, + appBundleIdentifier: appBundleIdentifier, + locales: locales + ) + } + + /// Sets up the application for each test pass, localizing from multiple bundles + /// + /// This method should be called from the subclass's setup() method. Use this form + /// when the YAML under test lives in more than one bundle — typically the test + /// harness's own bundle plus another target's resources; the localizations of all + /// *bundles* merge into one store. For a single bundle, + /// ``setUp(bundle:resourceDirectoryName:appBundleIdentifier:locales:)`` is available. + /// + /// ## Example + /// + /// ``` + /// @MainActor + /// class MyViewModelDisplayTestCase: ViewModelDisplayTestCase { + /// + /// override func setUp() async throws { + /// try await super.setUp( + /// bundles: [Bundle.main, MyAppServerResources.bundle], + /// resourceDirectoryName: "", + /// appBundleIdentifier: "com.mycompany.myapp" + /// ) + /// + /// continueAfterFailure = false // Stop the test and move on + /// } + /// } + /// ``` + /// + /// > View tests never require YAML to be present: any localized string that has no + /// > translation in *bundles* — a server-hosted *ViewModel*'s strings, for example — + /// > resolves to visible placeholder text derived from its key, so the element keeps + /// > its surface area and stays reachable by XCUI. Do not assert on placeholder + /// > content; localization completeness belongs in **LocalizableTestCase**'s + /// > *expectTranslations()*. + /// + /// - Parameters: + /// - bundles: The test harness's application bundle and other custom bundles containing YAML files + /// - resourceDirectoryName: The directory in the bundle to search for localizations (default: "") + /// - appBundleIdentifier: The application's bundle identifier + /// - locales: The locales to test (default: en) + public func setUp( + bundles: [Bundle], + resourceDirectoryName: String = "", + appBundleIdentifier: String, + locales: Set? = nil ) async throws { try await super.setUp() do { locStore = try KeyEchoLocalizationStore( - wrapping: bundle.yamlLocalization( + wrapping: bundles.yamlLocalization( resourceDirectoryName: resourceDirectoryName ) )