Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,8 @@ src/SupportingFiles/Booket/GoogleService-Info.plist
# Performance traces and local probe workspace (large, generated)
.perf/

# CodeGraph local index (generated, per-working-tree SQLite data)
.codegraph/

# Claude Code scheduled-task runtime lock (per-machine, not shared)
.claude/scheduled_tasks.lock
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ If a referenced doc is missing, ask before assuming its contents.

---

## Repository intelligence

When CodeGraph is available, use it for broad repository exploration, symbol
relationship discovery, and impact analysis before falling back to wide
`rg`/file-reading sweeps.

Use this workflow:

- For architecture or unfamiliar-area questions, start with CodeGraph
`context`, `query`, `impact`, `callers`, or `callees`.
- For exact literal searches, narrow symbol lookups, and final evidence, use
`rg` and direct file reads.
- If CodeGraph reports stale or pending files, verify those files directly
before relying on the indexed result.
- Do not commit `.codegraph/`; it is a generated local SQLite index. Regenerate
it with `codegraph init -i` when missing, and use `codegraph sync` only when
working outside an active MCP watcher or after a branch switch / batch edit.

---

## Architecture guardrails

### Feature structure
Expand Down
141 changes: 128 additions & 13 deletions Projects/Feature/MainTab/Example/Sources/MainTabExampleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,29 @@ import CoreCaptureSessionInterface
import DomainGoalInterface
import FeatureMakeGoal
import FeatureMakeGoalInterface
import SharedDesignSystem
import SharedPerfTestingSupport

struct MainTabExampleView: View {
var body: some View {
MainTabView(
store: Store(
initialState: MainTabReducer.State(),
reducer: {
MainTabReducer()
}, withDependencies: {
$0.goalClient = .previewValue
$0.captureSessionClient = UITestMode.isEnabled ? .perfMock : .liveValue
$0.proofPhotoFactory = .liveValue
$0.goalDetailFactory = .liveValue
$0.makeGoalFactory = .liveValue
}
if ProcessInfo.processInfo.arguments.contains("-UITEST_DESIGN_SYSTEM_BOTTOM_SHEET_SCENARIO") {
DesignSystemBottomSheetScenarioView()
} else {
MainTabView(
store: Store(
initialState: MainTabReducer.State(),
reducer: {
MainTabReducer()
}, withDependencies: {
$0.goalClient = .previewValue
$0.captureSessionClient = UITestMode.isEnabled ? .perfMock : .liveValue
$0.proofPhotoFactory = .liveValue
$0.goalDetailFactory = .liveValue
$0.makeGoalFactory = .liveValue
}
)
)
)
}
}
}

Expand All @@ -50,3 +55,113 @@ private extension CaptureSessionClient {
switchFlash: { _ in }
)
}

private struct DesignSystemBottomSheetScenarioView: View {
@State private var selectedTab: TXTabItem = .home
@State private var selectedDate = TXCalendarDate(year: 2026, month: 5, day: 28)
@State private var isBottomSheetPresented = false
@State private var completedCount = 0
@State private var selfRunStep = 0
@State private var hasStartedSelfRun = false

var body: some View {
TXTabBarContainer(selectedItem: $selectedTab) {
scenarioContent(title: "홈")
.tag(TXTabItem.home)

scenarioContent(title: "통계")
.tag(TXTabItem.statistics)
}
.txBottomSheet(
isPresented: $isBottomSheetPresented,
showDragIndicator: true
) {
TXCalendarBottomSheet(
selectedDate: $selectedDate,
completeButtonText: "완료",
onComplete: {
completedCount += 1
isBottomSheetPresented = false
}
)
.overlay(alignment: .topLeading) {
Color.clear
.frame(width: 1, height: 1)
.accessibilityIdentifier(
"example.bottom-sheet.calendar-month.\(selectedDate.formattedYearDashMonth)"
)
}
}
.overlay(alignment: .topLeading) {
Color.clear
.frame(width: 1, height: 1)
.accessibilityIdentifier("example.bottom-sheet.completed-count.\(completedCount)")
}
.overlay(alignment: .topLeading) {
Color.clear
.frame(width: 1, height: 1)
.accessibilityIdentifier("example.bottom-sheet.self-run-step.\(selfRunStep)")
}
.task {
await runCalendarBottomSheetSelfRunIfNeeded()
}
}

private func scenarioContent(title: String) -> some View {
VStack(spacing: Spacing.spacing6) {
Text(title)
.typography(.t1_18eb)
.foregroundStyle(Color.Gray.gray500)

Button("캘린더 바텀시트 열기") {
isBottomSheetPresented = true
}
.accessibilityIdentifier("example.bottom-sheet.present-button")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.Common.white)
.overlay(alignment: .topLeading) {
Button {
triggerQuickRepresentRace()
} label: {
Color.clear
.frame(width: 44, height: 44)
}
.accessibilityIdentifier("example.bottom-sheet.quick-represent-button")
}
}

private func triggerQuickRepresentRace() {
isBottomSheetPresented = true

Task { @MainActor in
try? await Task.sleep(for: .milliseconds(50))
isBottomSheetPresented = false
try? await Task.sleep(for: .milliseconds(50))
isBottomSheetPresented = true
}
}

@MainActor
private func runCalendarBottomSheetSelfRunIfNeeded() async {
guard UITestMode.isSwiftUISelfRunCalendarBottomSheet, !hasStartedSelfRun else { return }
hasStartedSelfRun = true

try? await Task.sleep(for: .milliseconds(900))
for iteration in 1...4 {
selfRunStep = (iteration * 10) + 1
isBottomSheetPresented = true
try? await Task.sleep(for: .milliseconds(650))

selfRunStep = (iteration * 10) + 2
selectedDate.goToNextMonth()
try? await Task.sleep(for: .milliseconds(250))

selfRunStep = (iteration * 10) + 3
isBottomSheetPresented = false
try? await Task.sleep(for: .milliseconds(450))
}

selfRunStep = 999
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,132 @@ final class MainTabExampleSmokeTests: XCTestCase {
_ = XCUIApplication.launchForPerf(seed: "default")
waitForFeatureReady("main-tab")
}

func testCalendarBottomSheetCoversCustomTabBarAndCompletes() {
let app = launchDesignSystemBottomSheetScenario()
waitForFeatureReady("main-tab")

let homeTab = app.buttons[DesignSystemBottomSheetScenarioID.homeTab]
let statisticsTab = app.buttons[DesignSystemBottomSheetScenarioID.statisticsTab]
XCTAssertTrue(homeTab.waitForExistence(timeout: 5))
XCTAssertTrue(statisticsTab.waitForExistence(timeout: 5))
let tabBarFrame = homeTab.frame.union(statisticsTab.frame)
attachScreenshot(named: "01-tabbar-baseline")

app.buttons[DesignSystemBottomSheetScenarioID.presentButton].tap()

let sheet = app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.sheetContent]
XCTAssertTrue(sheet.waitForExistence(timeout: 5))
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.calendarSheet].exists)
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.dragArea].exists)
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.calendarMonth("2026-05")].exists)
XCTAssertLessThan(sheet.frame.minY, tabBarFrame.maxY)
XCTAssertGreaterThanOrEqual(sheet.frame.maxY, tabBarFrame.maxY - 1)
attachScreenshot(named: "02-sheet-over-tabbar")

app.buttons[DesignSystemBottomSheetScenarioID.calendarNextButton].tap()
XCTAssertTrue(
app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.calendarMonth("2026-06")]
.waitForExistence(timeout: 2)
)
attachScreenshot(named: "03-calendar-next-month")

app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.completeButton].tap()
XCTAssertTrue(sheet.waitForNonExistence(timeout: 3))
XCTAssertTrue(homeTab.waitForExistence(timeout: 3))
XCTAssertTrue(statisticsTab.waitForExistence(timeout: 3))
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.completedCount(1)].exists)
attachScreenshot(named: "04-dismissed-tabbar-restored")
}

func testBottomSheetBackdropDismissesAndCanPresentRepeatedly() {
let app = launchDesignSystemBottomSheetScenario()
waitForFeatureReady("main-tab")

let presentButton = app.buttons[DesignSystemBottomSheetScenarioID.presentButton]
XCTAssertTrue(presentButton.waitForExistence(timeout: 5))

presentButton.tap()
let firstSheet = app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.sheetContent]
XCTAssertTrue(firstSheet.waitForExistence(timeout: 5))
attachScreenshot(named: "01-first-presentation")

let backdrop = app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.backdrop]
XCTAssertTrue(backdrop.waitForExistence(timeout: 2))
backdrop.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
XCTAssertTrue(firstSheet.waitForNonExistence(timeout: 3))
attachScreenshot(named: "02-backdrop-dismissed")

presentButton.tap()
let secondSheet = app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.sheetContent]
XCTAssertTrue(secondSheet.waitForExistence(timeout: 5))
attachScreenshot(named: "03-second-presentation")

app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.completeButton].tap()
XCTAssertTrue(secondSheet.waitForNonExistence(timeout: 3))
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.completedCount(1)].exists)
attachScreenshot(named: "04-second-dismissed")
}

func testBottomSheetQuickRepresentDuringDismissKeepsSheetVisible() {
let app = launchDesignSystemBottomSheetScenario()
waitForFeatureReady("main-tab")

let quickRepresentButton = app.buttons[DesignSystemBottomSheetScenarioID.quickRepresentButton]
XCTAssertTrue(quickRepresentButton.waitForExistence(timeout: 5))

quickRepresentButton.tap()
let sheet = app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.sheetContent]
XCTAssertTrue(sheet.waitForExistence(timeout: 5))
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.calendarSheet].exists)
attachScreenshot(named: "01-quick-represent-sheet-visible")

app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.completeButton].tap()
XCTAssertTrue(sheet.waitForNonExistence(timeout: 3))
XCTAssertTrue(app.descendants(matching: .any)[DesignSystemBottomSheetScenarioID.completedCount(1)].exists)
attachScreenshot(named: "02-quick-represent-dismissed")
}

private func launchDesignSystemBottomSheetScenario() -> XCUIApplication {
let app = XCUIApplication()
app.launchArguments.append(contentsOf: [
"-UITEST",
"-UITEST_SEED", "default",
"-UITEST_WAIT_READY",
"-UITEST_DISABLE_ANIMATIONS",
"-UITEST_DESIGN_SYSTEM_BOTTOM_SHEET_SCENARIO"
])
app.launch()
return app
}

private func attachScreenshot(named name: String) {
let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
}

private enum DesignSystemBottomSheetScenarioID {
static let presentButton = "example.bottom-sheet.present-button"
static let quickRepresentButton = "example.bottom-sheet.quick-represent-button"
static let completedCountPrefix = "example.bottom-sheet.completed-count"
static let calendarMonthPrefix = "example.bottom-sheet.calendar-month"
static let sheetContent = "tx.bottom-sheet.content"
static let backdrop = "tx.bottom-sheet.backdrop"
static let dragArea = "tx.bottom-sheet.drag-area"
static let calendarSheet = "tx.calendar-bottom-sheet"
static let completeButton = "tx.calendar-bottom-sheet.complete-button"
static let calendarNextButton = "tx.calendar.month-navigation.next-button"
static let homeTab = "tx.tab-bar.item.home"
static let statisticsTab = "tx.tab-bar.item.statistics"

static func completedCount(_ count: Int) -> String {
"\(completedCountPrefix).\(count)"
}

static func calendarMonth(_ yearDashMonth: String) -> String {
"\(calendarMonthPrefix).\(yearDashMonth)"
}
}
3 changes: 2 additions & 1 deletion Projects/Feature/MainTab/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ let project = Project.makeModule(
),
dependencies: [
.feature,
.core(implements: .captureSession)
.core(implements: .captureSession),
.shared(implements: .designSystem)
]
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private extension TXTabBar {
.padding(.top, Constants.topPadding)
}
.buttonStyle(.plain)
.accessibilityIdentifier("tx.tab-bar.item.\(item.accessibilityIdentifier)")
}
}

Expand All @@ -70,3 +71,18 @@ private extension TXTabBar {
static let labelFont: TypographyToken = .c2_11b
}
}

private extension TXTabItem {
var accessibilityIdentifier: String {
switch self {
case .home:
return "home"

case .statistics:
return "statistics"

case .couple:
return "couple"
}
}
}
Loading
Loading