diff --git a/.github/workflows/ios-pr-coverage.yml b/.github/workflows/ios-pr-coverage.yml index 856f29de..48899298 100644 --- a/.github/workflows/ios-pr-coverage.yml +++ b/.github/workflows/ios-pr-coverage.yml @@ -94,6 +94,8 @@ jobs: run: ./Scripts/prepare-ios-simulator.sh # Tuist가 테스트 산출물을 원격 shard storage에 올리고 matrix JSON을 GITHUB_OUTPUT으로 내보낸다. + # 소스 경로 매핑은 xctestrun의 원본 소스 목록과 달라져 xccov가 0/0을 기록한다. + # 커버리지 빌드만 원본 경로를 유지하고 컴파일 캐시는 계속 사용한다. - name: Build and plan test shards id: build continue-on-error: true @@ -113,6 +115,10 @@ jobs: -destination "$SIMULATOR_DESTINATION" \ -derivedDataPath "$CI_DERIVED_DATA" \ -enableCodeCoverage YES \ + SWIFT_ENABLE_PREFIX_MAPPING=NO \ + SWIFT_ENABLE_PROJECT_PREFIX_MAPPING=NO \ + CLANG_ENABLE_PREFIX_MAPPING=NO \ + CLANG_ENABLE_PROJECT_PREFIX_MAPPING=NO \ -collect-test-diagnostics never \ ONLY_ACTIVE_ARCH=YES \ COMPILATION_CACHE_ENABLE_CACHING="$XCODE_COMPILATION_CACHE_ENABLED" \ diff --git a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift index cbca6d08..a9cf7047 100644 --- a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift +++ b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift @@ -9,7 +9,7 @@ import Foundation import ProjectDescription public extension String { - static func appVersion(version: String = "1.1.0") -> String { + static func appVersion(version: String = "1.1.1") -> String { return version } diff --git a/Projects/App/Sources/Navigation/OnBoarding/OnBoardingCoordinator.swift b/Projects/App/Sources/Navigation/OnBoarding/OnBoardingCoordinator.swift index b1348071..9009832e 100644 --- a/Projects/App/Sources/Navigation/OnBoarding/OnBoardingCoordinator.swift +++ b/Projects/App/Sources/Navigation/OnBoarding/OnBoardingCoordinator.swift @@ -86,8 +86,7 @@ extension OnBoardingCoordinator { .routeAction(id: _, action: .selectPart(.delegate(.presentBack))), .routeAction(id: _, action: .selectManaging(.delegate(.presentBack))), .routeAction(id: _, action: .selectTeam(.delegate(.presentBack))): - state.routes.goBack() - return .none + return .send(.view(.backAction)) // MARK: - 이름 입력 diff --git a/Projects/App/Tests/Sources/AppStateTests.swift b/Projects/App/Tests/Sources/AppStateTests.swift index 2b610464..b0ba5d72 100644 --- a/Projects/App/Tests/Sources/AppStateTests.swift +++ b/Projects/App/Tests/Sources/AppStateTests.swift @@ -16,6 +16,34 @@ import TCAFlow @Suite("App State") struct AppStateTests { + @MainActor + @Test("이름 뒤로가기는 로그인으로 나가지 않고 초대 코드로 돌아온다") + func nameBackReturnsToInviteCode() async { + var onboarding = OnBoardingCoordinator.State() + onboarding.routes.append(.push(.onBoardingName(.init()))) + var state = AuthCoordinator.State() + state.routes.append(.push(.onboarding(onboarding))) + let store = Store(initialState: state) { AuthCoordinator() } + + await store.send(.router(.routeAction( + id: 1, + action: .onboarding(.router(.routeAction( + id: 1, action: .onBoardingName(.delegate(.presentBack)) + ))) + ))).finish() + + #expect(store.routes.count == 2) + guard case let .onboarding(result) = store.routes.last?.screen else { + Issue.record("이름 뒤로가기 후에도 온보딩이 유지되어야 한다") + return + } + #expect(result.routes.count == 1) + guard case .InviteCode = result.routes.first?.screen else { + Issue.record("초대 코드 화면으로 돌아와야 한다") + return + } + } + @MainActor @Test("중첩 가입 라우터는 이름 다음 직무 화면까지 표시한다") func nestedOnboardingDisplaysSecondPush() async throws { diff --git a/Projects/Feature/OnBoarding/Sources/ SelectPart/View/SelectPartView.swift b/Projects/Feature/OnBoarding/Sources/ SelectPart/View/SelectPartView.swift index ec8ba045..5eeafcd6 100644 --- a/Projects/Feature/OnBoarding/Sources/ SelectPart/View/SelectPartView.swift +++ b/Projects/Feature/OnBoarding/Sources/ SelectPart/View/SelectPartView.swift @@ -31,7 +31,7 @@ public struct SelectPartView: View { Spacer() .frame(height: 12) - StepNavigationBar(activeStep: 2) { + StepNavigationBar().activeStep(2).buttonAction { store.send(.delegate(.presentBack)) } diff --git a/Projects/Feature/OnBoarding/Sources/OnBoardingName/View/ OnBoardingNameView.swift b/Projects/Feature/OnBoarding/Sources/OnBoardingName/View/ OnBoardingNameView.swift index 3f57973b..401d1c1c 100644 --- a/Projects/Feature/OnBoarding/Sources/OnBoardingName/View/ OnBoardingNameView.swift +++ b/Projects/Feature/OnBoarding/Sources/OnBoardingName/View/ OnBoardingNameView.swift @@ -37,9 +37,11 @@ public struct OnBoardingNameView: View { Spacer() .frame(height: 12) - StepNavigationBar(activeStep: 1) { - store.send(.delegate(.presentBack)) - } + StepNavigationBar() + .activeStep(1) + .buttonAction { + store.send(.delegate(.presentBack)) + } ScrollView { signUpNameText() diff --git a/Projects/Feature/OnBoarding/Sources/SelectManaging/View/SelectManagingView.swift b/Projects/Feature/OnBoarding/Sources/SelectManaging/View/SelectManagingView.swift index 2d77173d..ca80f194 100644 --- a/Projects/Feature/OnBoarding/Sources/SelectManaging/View/SelectManagingView.swift +++ b/Projects/Feature/OnBoarding/Sources/SelectManaging/View/SelectManagingView.swift @@ -31,9 +31,11 @@ public struct SelectManagingView: View { Spacer() .frame(height: 12) - StepNavigationBar(activeStep: 3) { - store.send(.delegate(.presentBack)) - } + StepNavigationBar() + .activeStep(3) + .buttonAction { + store.send(.delegate(.presentBack)) + } // 목록을 받아오는 동안 실제 화면과 같은 자리에서 스켈레톤을 보여준다. switch store.viewState { diff --git a/Projects/Feature/OnBoarding/Sources/SelectTeam/View/SelectTeamView.swift b/Projects/Feature/OnBoarding/Sources/SelectTeam/View/SelectTeamView.swift index d6faf081..1b69449d 100644 --- a/Projects/Feature/OnBoarding/Sources/SelectTeam/View/SelectTeamView.swift +++ b/Projects/Feature/OnBoarding/Sources/SelectTeam/View/SelectTeamView.swift @@ -31,9 +31,11 @@ public struct SelectTeamView: View { Spacer() .frame(height: 12) - StepNavigationBar(activeStep: 3) { - store.send(.delegate(.presentBack)) - } + StepNavigationBar() + .activeStep(3) + .buttonAction { + store.send(.delegate(.presentBack)) + } // 목록을 받아오는 동안 실제 화면과 같은 자리에서 스켈레톤을 보여준다. switch store.viewState { diff --git a/Projects/Feature/OnBoarding/Tests/Sources/OnBoardingNameReducerTests.swift b/Projects/Feature/OnBoarding/Tests/Sources/OnBoardingNameReducerTests.swift index 467e284a..4fac2712 100644 --- a/Projects/Feature/OnBoarding/Tests/Sources/OnBoardingNameReducerTests.swift +++ b/Projects/Feature/OnBoarding/Tests/Sources/OnBoardingNameReducerTests.swift @@ -16,12 +16,16 @@ import ComposableArchitecture struct OnBoardingNameReducerTests { @Test("입력한 이름으로 다음 이동을 요청한다", arguments: ["서원지", "가나다라마"]) func enteredNameNavigates(name: String) async { - let store = TestStore(initialState: OnBoardingNameFeature.State()) { + var state = OnBoardingNameFeature.State() + // 매개변수 케이스끼리 같은 영속 키의 세션을 공유하지 않도록 분리한다. + state.$userSession = Shared(value: .empty) + let store = TestStore(initialState: state) { OnBoardingNameFeature() } await store.send(.view(.nameChanged(name))) { $0.$userSession.withLock { $0.name = name } } + #expect(store.state.userSession.name == name) #expect(store.state.enableButton) await store.send(.view(.checkIsAvailableName)) await store.receive(\.delegate.presentSignUpPart) diff --git a/Projects/UI/DDDDesignKit/Sources/Component/Button/StepNavigationBar.swift b/Projects/UI/DDDDesignKit/Sources/Component/Button/StepNavigationBar.swift index 750f6547..4a56cbdf 100644 --- a/Projects/UI/DDDDesignKit/Sources/Component/Button/StepNavigationBar.swift +++ b/Projects/UI/DDDDesignKit/Sources/Component/Button/StepNavigationBar.swift @@ -8,27 +8,24 @@ import SwiftUI public struct StepNavigationBar: View { - private var activeStep: Int - private var buttonAction: () -> Void - - public init( - activeStep: Int, - buttonAction: @escaping () -> Void - ) { - self.activeStep = activeStep - self.buttonAction = buttonAction - } + private var activeStep = 1 + private var buttonAction: () -> Void = {} + + public init() {} public var body: some View { HStack { - Image(asset: .backButton) - .resizable() - .scaledToFit() - .frame(width: 12, height: 20) - .foregroundStyle(Color.gray400) - .onTapGesture { - buttonAction() - } + Button(action: buttonAction) { + Image(asset: .backButton) + .resizable() + .scaledToFit() + .frame(width: 12, height: 20) + .foregroundStyle(.gray400) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel("뒤로가기") Spacer() @@ -51,7 +48,6 @@ public struct StepNavigationBar: View { // MARK: - 체이닝 설정 // // 값 타입 사본을 돌려주므로 호출 순서에 영향받지 않는다. -// 기존 init 은 그대로 두어, 체이닝은 선택지로만 더한다. public extension StepNavigationBar { /// `activeStep` 을 바꾼 사본을 돌려준다. func activeStep(_ activeStep: Int) -> Self { diff --git a/Projects/UI/DDDDesignKit/Tests/Sources/DDDDesignKitTests.swift b/Projects/UI/DDDDesignKit/Tests/Sources/DDDDesignKitTests.swift index 138c0123..6c87c220 100644 --- a/Projects/UI/DDDDesignKit/Tests/Sources/DDDDesignKitTests.swift +++ b/Projects/UI/DDDDesignKit/Tests/Sources/DDDDesignKitTests.swift @@ -52,7 +52,7 @@ struct DDDDesignKitTests { } build(NavigationBackButton(buttonAction: {}).buttonAction({})) - build(StepNavigationBar(activeStep: 1, buttonAction: {}).activeStep(3).buttonAction({})) + build(StepNavigationBar().activeStep(3).buttonAction({})) build(CustomNavigationBackBar(buttonAction: {})) build( CustomNavigationBar(backAction: {}, addAction: {}, image: .plus) diff --git a/Scripts/ci-workflow-structure.test.js b/Scripts/ci-workflow-structure.test.js index dce4a2ed..e92fc562 100644 --- a/Scripts/ci-workflow-structure.test.js +++ b/Scripts/ci-workflow-structure.test.js @@ -28,6 +28,23 @@ function job(source, name, nextName) { return source.slice(start, end); } +test("커버리지 빌드는 원본 소스 경로를 유지하면서 컴파일 캐시를 사용한다", () => { + const build = job(read(".github/workflows/ios-pr-coverage.yml"), "build-test-shards", "test-shards"); + const tests = read("Scripts/run-isolated-module-tests.sh"); + for (const command of [build, tests]) { + assert.match(command, /-enableCodeCoverage YES/); + for (const setting of [ + "SWIFT_ENABLE_PREFIX_MAPPING", + "SWIFT_ENABLE_PROJECT_PREFIX_MAPPING", + "CLANG_ENABLE_PREFIX_MAPPING", + "CLANG_ENABLE_PROJECT_PREFIX_MAPPING", + ]) { + assert.ok(command.includes(`${setting}=NO`), setting); + } + assert.match(command, /COMPILATION_CACHE_ENABLE_CACHING=/); + } +}); + test("iOS workflow는 공통 runner setup action을 사용한다", () => { const action = read(".github/actions/setup-ios-runner/action.yml"); assert.match(action, /using: composite/); diff --git a/Scripts/run-isolated-module-tests.sh b/Scripts/run-isolated-module-tests.sh index c0349ba1..2430d1d3 100755 --- a/Scripts/run-isolated-module-tests.sh +++ b/Scripts/run-isolated-module-tests.sh @@ -65,6 +65,10 @@ mise exec -- tuist test run "$TEST_SCHEME" \ -destination "$SIMULATOR_DESTINATION" \ -derivedDataPath "$CI_DERIVED_DATA" \ -enableCodeCoverage YES \ + SWIFT_ENABLE_PREFIX_MAPPING=NO \ + SWIFT_ENABLE_PROJECT_PREFIX_MAPPING=NO \ + CLANG_ENABLE_PREFIX_MAPPING=NO \ + CLANG_ENABLE_PROJECT_PREFIX_MAPPING=NO \ -retry-tests-on-failure \ -test-iterations 2 \ -collect-test-diagnostics never \