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

Some small improvements to Standups. #2333

Merged
merged 3 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "245d527002f29c5a616c5b7131f6a50ac7f41cbf",
"version" : "0.8.6"
"revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865",
"version" : "0.9.0"
}
}
],
Expand Down
5 changes: 4 additions & 1 deletion Examples/Standups/Standups/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ struct StandupsApp: App {
// dependencies for the duration of the test (e.g. the data manager). We do not really
// recommend performing UI tests in general, but we do want to demonstrate how it can be
// done.
if _XCTIsTesting || ProcessInfo.processInfo.environment["UITesting"] == "true" {
if ProcessInfo.processInfo.environment["UITesting"] == "true" {
UITestingView()
} else if _XCTIsTesting {
// NB: Don't run application when testing so that it doesn't interfere with tests.
EmptyView()
} else {
AppView(
store: Store(initialState: AppFeature.State()) {
Expand Down
26 changes: 15 additions & 11 deletions Examples/Standups/Standups/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ struct AppFeature: Reducer {
}
Reduce<State, Action> { state, action in
switch action {
case let .path(.popFrom(id)):
guard case let .some(.detail(detailState)) = state.path[id: id]
else { return .none }
state.standupsList.standups[id: detailState.standup.id]? = detailState.standup
return .none

case let .path(.element(id, .detail(.delegate(delegateAction)))):
guard case let .some(.detail(detailState)) = state.path[id: id]
else { return .none }
Expand All @@ -42,18 +36,25 @@ struct AppFeature: Reducer {
state.standupsList.standups.remove(id: detailState.standup.id)
return .none

case let .standupUpdated(standup):
state.standupsList.standups[id: standup.id] = standup
return .none

case .startMeeting:
state.path.append(.record(RecordMeeting.State(standup: detailState.standup)))
return .none
}

case let .path(.element(id, .record(.delegate(delegateAction)))):
case let .path(.element(_, .record(.delegate(delegateAction)))):
switch delegateAction {
case let .save(transcript: transcript):
state.path.pop(from: id)

guard let id = state.path.ids.last
else { return .none }
guard let id = state.path.ids.dropLast().last
else {
XCTFail("""
Record meeting is the only element in the stack. A detail feature should proceed it.
""")
stephencelis marked this conversation as resolved.
Show resolved Hide resolved
return .none
}

state.path[id: id, case: /Path.State.detail]?.standup.meetings.insert(
Meeting(
Expand All @@ -63,6 +64,9 @@ struct AppFeature: Reducer {
),
at: 0
)
guard let standup = state.path[id: id, case: /Path.State.detail]?.standup
else { return .none }
state.standupsList.standups[id: standup.id] = standup
return .none
}

Expand Down
20 changes: 8 additions & 12 deletions Examples/Standups/Standups/RecordMeeting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ struct RecordMeeting: Reducer {
@Dependency(\.dismiss) var dismiss
@Dependency(\.speechClient) var speechClient

private enum CancelID { case timer }

var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
switch action {
Expand All @@ -48,7 +46,10 @@ struct RecordMeeting: Reducer {
}

case .alert(.presented(.confirmSave)):
return self.meetingFinished(transcript: state.transcript)
return .run { [transcript = state.transcript] send in
await send(.delegate(.save(transcript: transcript)))
await self.dismiss()
}

case .alert:
return .none
Expand Down Expand Up @@ -89,7 +90,6 @@ struct RecordMeeting: Reducer {
}
}
}
.cancellable(id: CancelID.timer)

case .timerTick:
guard state.alert == nil
Expand All @@ -100,7 +100,10 @@ struct RecordMeeting: Reducer {
let secondsPerAttendee = Int(state.standup.durationPerAttendee.components.seconds)
if state.secondsElapsed.isMultiple(of: secondsPerAttendee) {
if state.speakerIndex == state.standup.attendees.count - 1 {
return self.meetingFinished(transcript: state.transcript)
return .run { [transcript = state.transcript] send in
await send(.delegate(.save(transcript: transcript)))
await self.dismiss()
}
}
state.speakerIndex += 1
}
Expand Down Expand Up @@ -138,13 +141,6 @@ struct RecordMeeting: Reducer {
await send(.timerTick)
}
}

private func meetingFinished(transcript: String) -> Effect<Action> {
.merge(
.cancel(id: CancelID.timer),
.send(.delegate(.save(transcript: transcript)))
)
}
}

struct RecordMeetingView: View {
Expand Down
6 changes: 6 additions & 0 deletions Examples/Standups/Standups/StandupDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct StandupDetail: Reducer {

enum Delegate: Equatable {
case deleteStandup
case standupUpdated(Standup)
case startMeeting
}
}
Expand Down Expand Up @@ -116,6 +117,11 @@ struct StandupDetail: Reducer {
.ifLet(\.$destination, action: /Action.destination) {
Destination()
}
.onChange(of: \.standup) { oldValue, newValue in
Reduce { state, action in
.send(.delegate(.standupUpdated(newValue)))
}
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions Examples/Standups/StandupsTests/AppFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,9 @@ final class AppFeatureTests: XCTestCase {
}
}

await store.send(.path(.popFrom(id: 0))) {
$0.path = StackState()
await store.receive(.path(.element(id: 0, action: .detail(.delegate(.standupUpdated(standup)))))) {
$0.standupsList.standups[0].title = "Blob"
}
.finish()

var savedStandup = standup
savedStandup.title = "Blob"
Expand Down Expand Up @@ -145,7 +143,6 @@ final class AppFeatureTests: XCTestCase {
.element(id: 1, action: .record(.delegate(.save(transcript: "I completed the project"))))
)
) {
$0.path.pop(to: 0)
$0.path[id: 0, case: /AppFeature.Path.State.detail]?.standup.meetings = [
Meeting(
id: Meeting.ID(UUID(0)),
Expand All @@ -154,5 +151,8 @@ final class AppFeatureTests: XCTestCase {
)
]
}
await store.receive(.path(.popFrom(id: 1))) {
XCTAssertEqual($0.path.count, 1)
}
}
}
50 changes: 37 additions & 13 deletions Examples/Standups/StandupsTests/RecordMeetingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import XCTest
final class RecordMeetingTests: XCTestCase {
func testTimer() async throws {
let clock = TestClock()
let dismissed = self.expectation(description: "dismissed")

let store = TestStore(
initialState: RecordMeeting.State(
Expand All @@ -24,10 +25,11 @@ final class RecordMeetingTests: XCTestCase {
RecordMeeting()
} withDependencies: {
$0.continuousClock = clock
$0.dismiss = DismissEffect { dismissed.fulfill() }
$0.speechClient.authorizationStatus = { .denied }
}

await store.send(.onTask)
let onTask = await store.send(.onTask)

await clock.advance(by: .seconds(1))
await store.receive(.timerTick) {
Expand Down Expand Up @@ -73,9 +75,15 @@ final class RecordMeetingTests: XCTestCase {

// NB: this improves on the onMeetingFinished pattern from vanilla SwiftUI
await store.receive(.delegate(.save(transcript: "")))

await self.fulfillment(of: [dismissed])
await onTask.cancel()
}

func testRecordTranscript() async throws {
let clock = TestClock()
let dismissed = self.expectation(description: "dismissed")

let store = TestStore(
initialState: RecordMeeting.State(
standup: Standup(
Expand All @@ -91,7 +99,8 @@ final class RecordMeetingTests: XCTestCase {
) {
RecordMeeting()
} withDependencies: {
$0.continuousClock = ImmediateClock()
$0.continuousClock = clock
$0.dismiss = DismissEffect { dismissed.fulfill() }
$0.speechClient.authorizationStatus = { .authorized }
$0.speechClient.startTask = { _ in
AsyncThrowingStream { continuation in
Expand All @@ -106,7 +115,7 @@ final class RecordMeetingTests: XCTestCase {
}
}

await store.send(.onTask)
let onTask = await store.send(.onTask)

await store.receive(
.speechResult(
Expand All @@ -117,6 +126,7 @@ final class RecordMeetingTests: XCTestCase {
}

await store.withExhaustivity(.off(showSkippedAssertions: true)) {
await clock.advance(by: .seconds(6))
await store.receive(.timerTick)
await store.receive(.timerTick)
await store.receive(.timerTick)
Expand All @@ -126,36 +136,42 @@ final class RecordMeetingTests: XCTestCase {
}

await store.receive(.delegate(.save(transcript: "I completed the project")))

await self.fulfillment(of: [dismissed])
await onTask.cancel()
}

func testEndMeetingSave() async throws {
let clock = TestClock()
let dismissed = self.expectation(description: "dismissed")

let store = TestStore(initialState: RecordMeeting.State(standup: .mock)) {
RecordMeeting()
} withDependencies: {
$0.continuousClock = clock
$0.dismiss = DismissEffect { dismissed.fulfill() }
$0.speechClient.authorizationStatus = { .denied }
}

await store.send(.onTask)
let onTask = await store.send(.onTask)

await store.send(.endMeetingButtonTapped) {
$0.alert = .endMeeting(isDiscardable: true)
}

await clock.advance(by: .seconds(1))
await clock.advance(by: .seconds(3))
await store.receive(.timerTick)
await clock.advance(by: .seconds(1))
await store.receive(.timerTick)
await clock.advance(by: .seconds(1))
await store.receive(.timerTick)

await store.send(.alert(.presented(.confirmSave))) {
$0.alert = nil
}

await store.receive(.delegate(.save(transcript: "")))

await self.fulfillment(of: [dismissed])
await onTask.cancel()
}

func testEndMeetingDiscard() async throws {
Expand Down Expand Up @@ -186,6 +202,7 @@ final class RecordMeetingTests: XCTestCase {

func testNextSpeaker() async throws {
let clock = TestClock()
let dismissed = self.expectation(description: "dismissed")

let store = TestStore(
initialState: RecordMeeting.State(
Expand All @@ -203,10 +220,11 @@ final class RecordMeetingTests: XCTestCase {
RecordMeeting()
} withDependencies: {
$0.continuousClock = clock
$0.dismiss = DismissEffect { dismissed.fulfill() }
$0.speechClient.authorizationStatus = { .denied }
}

await store.send(.onTask)
let onTask = await store.send(.onTask)

await store.send(.nextButtonTapped) {
$0.speakerIndex = 1
Expand All @@ -227,10 +245,13 @@ final class RecordMeetingTests: XCTestCase {
}

await store.receive(.delegate(.save(transcript: "")))
await self.fulfillment(of: [dismissed])
await onTask.cancel()
}

func testSpeechRecognitionFailure_Continue() async throws {
let clock = TestClock()
let dismissed = self.expectation(description: "dismissed")

let store = TestStore(
initialState: RecordMeeting.State(
Expand All @@ -248,6 +269,7 @@ final class RecordMeetingTests: XCTestCase {
RecordMeeting()
} withDependencies: {
$0.continuousClock = clock
$0.dismiss = DismissEffect { dismissed.fulfill() }
$0.speechClient.authorizationStatus = { .authorized }
$0.speechClient.startTask = { _ in
AsyncThrowingStream {
Expand All @@ -263,7 +285,7 @@ final class RecordMeetingTests: XCTestCase {
}
}

await store.send(.onTask)
let onTask = await store.send(.onTask)

await store.receive(
.speechResult(
Expand All @@ -282,7 +304,7 @@ final class RecordMeetingTests: XCTestCase {
$0.alert = nil
}

await clock.run()
await clock.advance(by: .seconds(6))

store.exhaustivity = .off(showSkippedAssertions: true)
await store.receive(.timerTick)
Expand All @@ -294,12 +316,14 @@ final class RecordMeetingTests: XCTestCase {
store.exhaustivity = .on

await store.receive(.delegate(.save(transcript: "I completed the project ❌")))
await self.fulfillment(of: [dismissed])
await onTask.cancel()
}

func testSpeechRecognitionFailure_Discard() async throws {
let clock = TestClock()

let dismissed = self.expectation(description: "dismissed")

let store = TestStore(initialState: RecordMeeting.State(standup: .mock)) {
RecordMeeting()
} withDependencies: {
Expand All @@ -314,7 +338,7 @@ final class RecordMeetingTests: XCTestCase {
}
}

let task = await store.send(.onTask)
let onTask = await store.send(.onTask)

await store.receive(.speechFailure) {
$0.alert = .speechRecognizerFailed
Expand All @@ -325,6 +349,6 @@ final class RecordMeetingTests: XCTestCase {
}

await self.fulfillment(of: [dismissed])
await task.cancel()
await onTask.cancel()
}
}
2 changes: 2 additions & 0 deletions Examples/Standups/StandupsTests/StandupDetailTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,7 @@ final class StandupDetailTests: XCTestCase {
$0.destination = nil
$0.standup.title = "Blob's Meeting"
}

await store.receive(.delegate(.standupUpdated(standup)))
}
}