Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintenance #38

Merged
merged 20 commits into from
Dec 5, 2023
Merged
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
89 changes: 50 additions & 39 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,83 +9,94 @@ on: # Rebuild any PRs and main branch changes

jobs:
test-vsm:
runs-on: macos-12
runs-on: macos-13
steps:
- name: Prepare Xcode
uses: maxim-lobanov/setup-xcode@v1 # https://github.com/marketplace/actions/setup-xcode-version
with:
xcode-version: 14.2
xcode-version: 15.0.1

- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Build and Test VSM on macOS
- name: Build and Test VSM on macOS (Intel)
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
spm-package: ./
scheme: VSM
destination: platform=macOS,arch=x86_64
action: test

- name: Build and Test VSM on Mac Catalyst (Intel)
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
spm-package: ./
scheme: VSM
destination: platform=macOS,arch=x86_64,variant=Mac Catalyst
action: test

- name: Build and Test VSM on iOS
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
spm-package: ./
scheme: VSM
destination: platform=iOS Simulator,OS=latest,name=iPhone 14
destination: platform=iOS Simulator,OS=17.0.1,name=iPhone 15
action: test

- name: Build and Test VSM on watchOS
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
spm-package: ./
scheme: VSM
destination: platform=watchOS Simulator,OS=latest,name=Apple Watch SE (40mm) (2nd generation)
destination: platform=watchOS Simulator,OS=10.0,name=Apple Watch Series 9 (45mm)
action: test

- name: Build and Test VSM on tvOS
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
spm-package: ./
scheme: VSM
destination: platform=tvOS Simulator,OS=latest,name=Apple TV
destination: platform=tvOS Simulator,OS=17.0,name=Apple TV 4K (3rd generation) (at 1080p)
action: test

# The following jobs are disabled until further notice to unblock work
# Xcode currently has UI test runtime issues since Xcode 14.3
# These UI tests should be run manually by engineers until the Xcode runtime issues are resolved

test-swiftui-demo-app:
runs-on: macos-12
steps:
- name: Prepare Xcode
uses: maxim-lobanov/setup-xcode@v1 # https://github.com/marketplace/actions/setup-xcode-version
with:
xcode-version: 14.2
# test-swiftui-demo-app:
# runs-on: macos-13
# steps:
# - name: Prepare Xcode
# uses: maxim-lobanov/setup-xcode@v1 # https://github.com/marketplace/actions/setup-xcode-version
# with:
# xcode-version: 14.3.1 # Xcode 15 has UI test runtime issues

- name: Checkout
uses: actions/checkout@v3
# - name: Checkout
# uses: actions/checkout@v4

- name: Build and Test Demo App
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
project: ./Demos/Shopping/Shopping.xcodeproj
scheme: Shopping
destination: platform=iOS Simulator,OS=latest,name=iPhone 14
action: test
# - name: Build and Test Demo App
# uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
# with:
# project: ./Demos/Shopping/Shopping.xcodeproj
# scheme: Shopping
# destination: platform=iOS Simulator,OS=17.0.1,name=iPhone 14
# action: test

test-uikit-demo-app:
runs-on: macos-12
steps:
- name: Prepare Xcode
uses: maxim-lobanov/setup-xcode@v1 # https://github.com/marketplace/actions/setup-xcode-version
with:
xcode-version: 14.2
# test-uikit-demo-app:
# runs-on: macos-13
# steps:
# - name: Prepare Xcode
# uses: maxim-lobanov/setup-xcode@v1 # https://github.com/marketplace/actions/setup-xcode-version
# with:
# xcode-version: 14.3.1 # Xcode 15 has UI test runtime issues

- name: Checkout
uses: actions/checkout@v3
# - name: Checkout
# uses: actions/checkout@v4

- name: (UIKit) Build and Test Demo App
uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
with:
project: ./Demos/Shopping/Shopping.xcodeproj
scheme: Shopping - UIKit
destination: platform=iOS Simulator,OS=latest,name=iPhone 14
action: test
# - name: (UIKit) Build and Test Demo App
# uses: sersoft-gmbh/xcodebuild-action@v2 # https://github.com/marketplace/actions/xcodebuild-action
# with:
# project: ./Demos/Shopping/Shopping.xcodeproj
# scheme: Shopping - UIKit
# destination: platform=iOS Simulator,OS=17.0.1,name=iPhone 14
# action: test
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
markdown:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: ⬇️ lint markdown files # Lints all markdown (.md) files
uses: avto-dev/markdown-lint@v1
with:
Expand All @@ -20,7 +20,7 @@ jobs:
renovate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: 🧼 lint renovate config # Validates changes to renovate.json config file
uses: suzuki-shunsuke/github-action-renovate-config-validator@v0.1.3
with:
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,28 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-12
runs-on: macos-13
steps:

- name: Prepare Xcode
uses: maxim-lobanov/setup-xcode@v1 # https://github.com/marketplace/actions/setup-xcode-version
with:
xcode-version: 14.2
xcode-version: 15.0.1

- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Build Documentation
run: ./Scripts/generate-docs.sh

- name: Setup Pages
uses: actions/configure-pages@v3
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v2
with:
path: './docs' # This path is coordinated with /generate-docs.sh

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v3
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
pull-requests: write
steps:
- name: 📆 mark stale PRs # Automatically marks inactive PRs as stale
uses: actions/stale@v7
uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/albertbori/TestableCombinePublishers.git",
"state" : {
"revision" : "c4581c15e3960af0b8e946193fc260a8deb128a3",
"version" : "1.0.4"
"revision" : "a053f58f21a0187817afd65b440911496809d21a",
"version" : "1.2.1"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions Demos/Shopping/ShoppingUITests/TestPages/SettingsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ struct SettingsPage: TestableUI, PushedPage {
let previousView: AccountTabPage

private var navBarTitle: XCUIElement { app.navigationBars["Settings"] }
private func toggle(for setting: Setting) -> XCUIElement { app.switches[setting.rawValue] }

// 12/4/23 Added `.switches.firstMatch` due to bug: https://stackoverflow.com/a/76063451/300408
private func toggle(for setting: Setting) -> XCUIElement { app.switches[setting.rawValue].switches.firstMatch }

init(app: XCUIApplication, previousView: AccountTabPage, file: StaticString = #file, line: UInt = #line) {
self.app = app
Expand Down
6 changes: 3 additions & 3 deletions Sources/VSM/ViewState/RenderedViewState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ import Combine
@propertyWrapper
public struct RenderedViewState<State> {

let renderedContainer: RenderedContainer<State>
let renderedContainer: RenderedContainer

// MARK: Encapsulating Properties

public var wrappedValue: State {
get { projectedValue.container.state }
}

public var projectedValue: RenderedContainer<State> {
public var projectedValue: RenderedContainer {
get { renderedContainer }
}

Expand Down Expand Up @@ -189,7 +189,7 @@ public struct RenderedViewState<State> {
@available(iOS 14.0, *)
public extension RenderedViewState {
/// Provides functions for observing and rendering state changes in UIKit views and view controllers
struct RenderedContainer<State> {
struct RenderedContainer {
/// The wrapped state container for managing changes in state
let container: StateContainer<State>
/// Implicitly used by UIKit views to automatically call the provided function when the state changes
Expand Down
100 changes: 45 additions & 55 deletions Tests/VSMTests/StateContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,70 +102,60 @@ class StateContainerTests: XCTestCase {

/// Asserts that different state observation types will progress one to another in this order: Publisher -> Async -> Sync -> Async -> Publisher.
func testAllActionTypesProgression() throws {
let mockPublishedAction: () -> AnyPublisher<MockState, Never> = {
return Deferred {
Just(MockState.bar)
.subscribe(on: DispatchQueue.global(qos: .background))
}
.eraseToAnyPublisher()
}
let mockAsyncAction: () async -> MockState = {
do {
try await Task.sleep(seconds: 0.1)
} catch {
XCTFail("Task sleep error: \(error)")
}
return .baz
}
let mockSyncAction: () -> MockState = {
.qux
}
let mockAsyncAction: (MockState) async -> MockState = { $0 }
let subject = StateContainer<MockState>(state: .foo)

test(subject, expect: [.foo, .bar], when: { $0.observe(mockPublishedAction()) })
test(subject, expect: [.bar, .baz], when: { $0.observeAsync({ await mockAsyncAction() }) })
test(subject, expect: [.baz, .qux], when: { $0.observe(mockSyncAction()) })
test(subject, expect: [.qux, .baz], when: { $0.observeAsync({ await mockAsyncAction() }) })
test(subject, expect: [.baz, .bar], when: { $0.observe(mockPublishedAction()) })
test(subject, expect: [.foo, .bar], when: { $0.observe(Just(MockState.bar).eraseToAnyPublisher()) })
test(subject, expect: [.bar, .baz], when: { $0.observeAsync({ await mockAsyncAction(.baz) }) })
test(subject, expect: [.baz, .qux], when: { $0.observe(.qux) })
test(subject, expect: [.qux, .baz], when: { $0.observeAsync({ await mockAsyncAction(.baz) }) })
test(subject, expect: [.baz, .bar], when: { $0.observe(Just(MockState.bar).eraseToAnyPublisher()) })
test(subject, expect: [.bar, .foo], when: { $0.observe(.foo) })
}

/// Validates that a publisher's delayed output will not change the state if another action has been (or is being) observed
func testAccidentalPublisherStateOverride() {
let mockPublishedAction: () -> AnyPublisher<MockState, Never> = {
let publisher = CurrentValueSubject<MockState, Never>(.bar)
DispatchQueue.global().async {
for newState in [MockState.corge, MockState.grault] {
Thread.sleep(forTimeInterval: 0.2)
publisher.value = newState
}
}
return publisher.eraseToAnyPublisher()
}
let mockAsyncAction: () async -> MockState = {
do {
try await Task.sleep(seconds: 0.5)
} catch {
XCTFail("Task sleep error: \(error)")
}
return .baz
}
let mockSyncAction: () -> MockState = {
.qux
}
let mockSyncAction2: () -> MockState = {
.quux
}
let mockAsyncAction: (MockState) async -> MockState = { $0 }
let subject = StateContainer<MockState>(state: .foo)
let negativeTest = subject.$state
.expectNot(.corge)
.expectNot(.grault)
let supersededPublisher = PassthroughSubject<MockState, Never>()

test(subject, expect: [.foo, .bar], when: { $0.observe(mockPublishedAction()) })
test(subject, expect: [.bar, .baz], when: { $0.observeAsync({ await mockAsyncAction() }) })
test(subject, expect: [.baz, .qux], when: { $0.observe(mockSyncAction()) })
test(subject, expect: [.qux, .quux], when: { $0.observe(mockSyncAction2()) })
// Subsequent publisher action
let publisherActionTest = subject.$state
.collect(4)
.expect([.foo, .bar, .baz, .foo])

negativeTest.waitForExpectations(timeout: 1)
subject.observe(supersededPublisher.eraseToAnyPublisher())
supersededPublisher.send(.bar)
subject.observe(CurrentValueSubject(.baz).eraseToAnyPublisher())
supersededPublisher.send(.grault) // <- This should not update the state
subject.observe(.foo) // <- This bookend allows us to test this scenario without using negative condition tests that wait the full timeout

publisherActionTest.waitForExpectations(timeout: 1)

// Subsequent synchronous action
let syncActionTest = subject.$state
.collect(4)
.expect([.foo, .bar, .baz, .foo])

subject.observe(supersededPublisher.eraseToAnyPublisher())
supersededPublisher.send(.bar)
subject.observe(.baz)
supersededPublisher.send(.grault) // <- This should not update the state
subject.observe(.foo) // <- This bookend allows us to test this scenario without using negative condition tests that wait the full timeout

syncActionTest.waitForExpectations(timeout: 1)

// Subsequent asynchronous action
let asyncActionTest = subject.$state
.collect(3)
.expect([.foo, .bar, .baz])

subject.observe(supersededPublisher.eraseToAnyPublisher())
supersededPublisher.send(.bar)
subject.observeAsync({ await mockAsyncAction(.baz) })
supersededPublisher.send(.grault) // <- This should not update the state, even tho it will complete before the observeAsync finishes

asyncActionTest.waitForExpectations(timeout: 1)
}

/// Validates that a async action's delayed output will not change the state if another action has been (or is being) observed
Expand Down
4 changes: 2 additions & 2 deletions Tests/VSMTests/ViewStateRenderingTests+ObserveDebounce.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ class ViewStateRenderingTests_ObserveDebounce: XCTestCase {
/// Asserts that multiple time-delayed action observations will each execute, if they are called far enough apart
func testDebounce_DefaultId_SingleAction_Delayed_MultipleCalls() async throws {
actionCallSite()
try await Task.sleep(seconds: 0.6)
try await Task.sleep(seconds: 1)
actionCallSite()
try await Task.sleep(seconds: 1) // wait for debounce timeout
try await Task.sleep(seconds: 2) // wait for debounce timeout

XCTAssertEqual(2, countableAction.count)
}
Expand Down