Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
@ -72,7 +72,17 @@ struct AndromidaApp: App {
|
||||
let settings = SettingsStore()
|
||||
_settingsStore = State(initialValue: settings)
|
||||
_categoryStore = State(initialValue: CategoryStore())
|
||||
_store = State(initialValue: RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings))
|
||||
|
||||
let ritualStore = RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings)
|
||||
if isUITesting, environment["UITEST_SEED_THREE_PRESETS"] == "1" {
|
||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[0]) // morning
|
||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[1]) // midday
|
||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[3]) // evening
|
||||
}
|
||||
if isUITesting, environment["UITEST_PRELOAD_DEMO_DATA"] == "1" {
|
||||
ritualStore.preloadDemoData()
|
||||
}
|
||||
_store = State(initialValue: ritualStore)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
@ -82,12 +92,13 @@ struct AndromidaApp: App {
|
||||
.ignoresSafeArea()
|
||||
|
||||
if hasCompletedSetupWizard {
|
||||
let uiTestInitialTab = uiTestRequestedInitialTab()
|
||||
// Main app - start on Rituals tab if just completed wizard
|
||||
RootView(
|
||||
store: store,
|
||||
settingsStore: settingsStore,
|
||||
categoryStore: categoryStore,
|
||||
initialTab: justCompletedWizard ? .rituals : .today
|
||||
initialTab: uiTestInitialTab ?? (justCompletedWizard ? .rituals : .today)
|
||||
)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
@ -111,4 +122,16 @@ struct AndromidaApp: App {
|
||||
.preferredColorScheme(settingsStore.theme.colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private func uiTestRequestedInitialTab() -> RootView.RootTab? {
|
||||
guard ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" else { return nil }
|
||||
switch ProcessInfo.processInfo.environment["UITEST_INITIAL_TAB"]?.lowercased() {
|
||||
case "today": return .today
|
||||
case "rituals": return .rituals
|
||||
case "insights": return .insights
|
||||
case "history": return .history
|
||||
case "settings": return .settings
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,6 +140,7 @@ struct SettingsView: View {
|
||||
) {
|
||||
ritualStore.preloadDemoData()
|
||||
}
|
||||
.accessibilityIdentifier("settings.debug.preloadDemoData")
|
||||
|
||||
SettingsRow(
|
||||
systemImage: "checkmark.circle.badge.xmark",
|
||||
|
||||
@ -8,6 +8,28 @@
|
||||
import XCTest
|
||||
|
||||
final class AndromidaUITests: XCTestCase {
|
||||
private let doneLabels = ["Done", "Terminé", "Listo"]
|
||||
private let settingsTabLabels = ["Settings", "Réglages", "Paramètres", "Ajustes"]
|
||||
private let tabLabelMap: [String: [String]] = [
|
||||
"Today": ["Today", "Aujourd'hui", "Hoy"],
|
||||
"Rituals": ["Rituals", "Rituels", "Rituales"],
|
||||
"Insights": ["Insights", "Aperçus", "Ideas"],
|
||||
"History": ["History", "Historique", "Historial"],
|
||||
"Settings": ["Settings", "Réglages", "Paramètres", "Ajustes"]
|
||||
]
|
||||
private let tabSymbolMap: [String: String] = [
|
||||
"Today": "sun.max.fill",
|
||||
"Rituals": "sparkles",
|
||||
"Insights": "chart.bar.fill",
|
||||
"History": "calendar",
|
||||
"Settings": "gearshape.fill"
|
||||
]
|
||||
private let preloadLabels = [
|
||||
"Preload 6 Months Demo Data",
|
||||
"Précharge 6 mois Données de démonstration",
|
||||
"Precarga 6 Meses Datos de demostración"
|
||||
]
|
||||
private let preloadPrefixLabels = ["Preload 6", "Précharge 6", "Precarga 6"]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
@ -155,14 +177,14 @@ final class AndromidaUITests: XCTestCase {
|
||||
@MainActor
|
||||
func testAppStorePortraitScreenshots() throws {
|
||||
let app = makeApp(resetDefaults: true, hasCompletedSetupWizard: true)
|
||||
app.launchEnvironment["UITEST_INITIAL_TAB"] = "settings"
|
||||
app.launch()
|
||||
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
|
||||
// Add three preset rituals across the day, then seed representative history from Debug settings.
|
||||
addThreePresetRitualsAcrossDay(app: app)
|
||||
// Ensure rituals exist before seeding demo history.
|
||||
assertRitualsPresentBeforePreload(app: app)
|
||||
preloadSixMonthsDemoDataFromDebug(app: app)
|
||||
ensureDataRichStateForScreenshots(app: app)
|
||||
|
||||
openTab(app: app, label: "Today", index: 0)
|
||||
XCTAssertFalse(app.staticTexts["No Active Rituals"].exists, "Today should show active rituals for App Store screenshots.")
|
||||
@ -188,6 +210,8 @@ final class AndromidaUITests: XCTestCase {
|
||||
private func makeApp(resetDefaults: Bool, hasCompletedSetupWizard: Bool) -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchEnvironment["UITEST_MODE"] = "1"
|
||||
app.launchEnvironment["UITEST_SEED_THREE_PRESETS"] = "1"
|
||||
app.launchEnvironment["UITEST_PRELOAD_DEMO_DATA"] = "1"
|
||||
if resetDefaults {
|
||||
app.launchEnvironment["UITEST_RESET_USER_DEFAULTS"] = "1"
|
||||
}
|
||||
@ -195,54 +219,20 @@ final class AndromidaUITests: XCTestCase {
|
||||
return app
|
||||
}
|
||||
|
||||
private func addThreePresetRitualsAcrossDay(app: XCUIApplication) {
|
||||
openTab(app: app, label: "Rituals", index: 1)
|
||||
|
||||
let addMenu = app.buttons["rituals.addMenu"]
|
||||
guard addMenu.waitForExistence(timeout: 8) else { return }
|
||||
addMenu.tap()
|
||||
|
||||
let browsePresets = app.buttons["rituals.browsePresets"]
|
||||
guard browsePresets.waitForExistence(timeout: 8) else { return }
|
||||
browsePresets.tap()
|
||||
|
||||
let presetTitles = ["Morning Hydration", "Midday Movement", "Evening Nutrition"]
|
||||
for title in presetTitles {
|
||||
if let preset = findElementWithVerticalSearch(
|
||||
app: app,
|
||||
label: title,
|
||||
timeoutPerAttempt: 1.0,
|
||||
swipeCount: 6
|
||||
) {
|
||||
if preset.isHittable {
|
||||
preset.tap()
|
||||
} else {
|
||||
preset.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
|
||||
let addButton = app.buttons["Add to My Rituals"]
|
||||
if addButton.waitForExistence(timeout: 8) {
|
||||
addButton.tap()
|
||||
}
|
||||
tapDoneInNavigationBar(app: app, title: title)
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
|
||||
}
|
||||
}
|
||||
|
||||
tapDoneInNavigationBar(app: app, title: "Preset Library")
|
||||
}
|
||||
|
||||
private func preloadSixMonthsDemoDataFromDebug(app: XCUIApplication) {
|
||||
openTab(app: app, label: "Settings", index: 4)
|
||||
// iPad tab/sidebars are highly variable in UI tests on iOS 26;
|
||||
// demo data is already guaranteed via launch environment.
|
||||
if app.windows.firstMatch.frame.width >= 1000 {
|
||||
return
|
||||
}
|
||||
|
||||
// Move into the lower Debug section before tapping preload.
|
||||
_ = findElementWithVerticalSearch(app: app, label: "Debug", timeoutPerAttempt: 0.5, swipeCount: 8)
|
||||
openTab(app: app, label: "Settings", index: 4)
|
||||
|
||||
if let preload = findElementWithVerticalSearch(
|
||||
app: app,
|
||||
label: "Preload 6 Months Demo Data",
|
||||
timeoutPerAttempt: 0.5,
|
||||
swipeCount: 10
|
||||
label: "settings.debug.preloadDemoData",
|
||||
timeoutPerAttempt: 0.3,
|
||||
swipeCount: 12
|
||||
) {
|
||||
if preload.isHittable {
|
||||
preload.tap()
|
||||
@ -250,21 +240,64 @@ final class AndromidaUITests: XCTestCase {
|
||||
preload.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(1.0))
|
||||
waitForHistoryDataSeed(app: app)
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForHistoryDataSeed(app: XCUIApplication) {
|
||||
for _ in 0..<6 {
|
||||
openTab(app: app, label: "History", index: 3)
|
||||
let nonZeroDay = app.buttons.matching(
|
||||
NSPredicate(format: "label CONTAINS[c] %@ AND NOT (label CONTAINS[c] %@)", "percent complete", "0 percent complete")
|
||||
).firstMatch
|
||||
if nonZeroDay.waitForExistence(timeout: 1.5) {
|
||||
return
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
||||
|
||||
if tapPreloadByPrefixSearch(app: app) {
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(1.0))
|
||||
return
|
||||
}
|
||||
|
||||
let preload = findAnyElementWithVerticalSearch(
|
||||
app: app,
|
||||
labels: preloadLabels,
|
||||
timeoutPerAttempt: 0.5,
|
||||
swipeCount: 10
|
||||
)
|
||||
|
||||
if let preload {
|
||||
if preload.isHittable {
|
||||
preload.tap()
|
||||
} else {
|
||||
preload.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(1.0))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func tapPreloadByPrefixSearch(app: XCUIApplication) -> Bool {
|
||||
func tapMatchingElement() -> Bool {
|
||||
let elements = app.descendants(matching: .any).allElementsBoundByIndex
|
||||
for element in elements where element.exists {
|
||||
let label = element.label
|
||||
if preloadPrefixLabels.contains(where: { label.contains($0) }) {
|
||||
if element.isHittable {
|
||||
element.tap()
|
||||
} else {
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if tapMatchingElement() { return true }
|
||||
for _ in 0..<12 {
|
||||
app.swipeUp()
|
||||
if tapMatchingElement() { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func assertRitualsPresentBeforePreload(app: XCUIApplication) {
|
||||
openTab(app: app, label: "Rituals", index: 1)
|
||||
XCTAssertFalse(
|
||||
app.staticTexts["No Active Rituals"].exists,
|
||||
"Expected seeded rituals before preload."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -356,35 +389,128 @@ final class AndromidaUITests: XCTestCase {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func findAnyElementWithVerticalSearch(
|
||||
app: XCUIApplication,
|
||||
labels: [String],
|
||||
timeoutPerAttempt: TimeInterval,
|
||||
swipeCount: Int
|
||||
) -> XCUIElement? {
|
||||
for label in labels {
|
||||
if let element = firstMatchIfExists(app: app, label: label, timeout: timeoutPerAttempt) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..<swipeCount {
|
||||
app.swipeUp()
|
||||
for label in labels {
|
||||
if let element = firstMatchIfExists(app: app, label: label, timeout: timeoutPerAttempt) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..<swipeCount {
|
||||
app.swipeDown()
|
||||
for label in labels {
|
||||
if let element = firstMatchIfExists(app: app, label: label, timeout: timeoutPerAttempt) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func tapDoneInNavigationBar(app: XCUIApplication, title: String) {
|
||||
let navDone = app.navigationBars[title].buttons["Done"].firstMatch
|
||||
if navDone.waitForExistence(timeout: 6) {
|
||||
for navTitle in [title] {
|
||||
for done in doneLabels {
|
||||
let navDone = app.navigationBars[navTitle].buttons[done].firstMatch
|
||||
if navDone.waitForExistence(timeout: 2) {
|
||||
navDone.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let anyDone = app.buttons["Done"].firstMatch
|
||||
if anyDone.waitForExistence(timeout: 4) {
|
||||
if let anyDone = firstExistingDoneButton(app: app, timeout: 4) {
|
||||
anyDone.tap()
|
||||
}
|
||||
}
|
||||
|
||||
private func openTab(app: XCUIApplication, label: String, index: Int) {
|
||||
let primary = app.tabBars.buttons[label].firstMatch
|
||||
if primary.waitForExistence(timeout: 8) {
|
||||
primary.tap()
|
||||
return
|
||||
private func firstExistingDoneButton(app: XCUIApplication, timeout: TimeInterval) -> XCUIElement? {
|
||||
firstExistingButton(app: app, labels: doneLabels, timeout: timeout)
|
||||
}
|
||||
|
||||
private func firstExistingButton(app: XCUIApplication, labels: [String], timeout: TimeInterval) -> XCUIElement? {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
for label in labels {
|
||||
let button = app.buttons[label].firstMatch
|
||||
if button.exists {
|
||||
return button
|
||||
}
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func openTab(app: XCUIApplication, label: String, index: Int) {
|
||||
let localizedLabels = tabLabelMap[label] ?? [label]
|
||||
|
||||
let tabButtons = app.tabBars.buttons
|
||||
if tabButtons.count > index {
|
||||
tabButtons.element(boundBy: index).tap()
|
||||
let indexed = tabButtons.element(boundBy: index)
|
||||
if indexed.exists {
|
||||
indexed.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let fallback = app.buttons[label].firstMatch
|
||||
if fallback.exists {
|
||||
fallback.tap()
|
||||
for localizedLabel in localizedLabels {
|
||||
let primary = app.tabBars.buttons[localizedLabel].firstMatch
|
||||
if primary.waitForExistence(timeout: 2) {
|
||||
primary.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for localizedLabel in localizedLabels {
|
||||
let sidebarButton = app.buttons[localizedLabel].firstMatch
|
||||
if sidebarButton.waitForExistence(timeout: 2) {
|
||||
sidebarButton.tap()
|
||||
return
|
||||
}
|
||||
let staticText = app.staticTexts[localizedLabel].firstMatch
|
||||
if staticText.waitForExistence(timeout: 1) {
|
||||
staticText.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let symbol = tabSymbolMap[label] {
|
||||
let symbolButton = app.buttons[symbol].firstMatch
|
||||
if symbolButton.waitForExistence(timeout: 2) {
|
||||
symbolButton.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for localizedLabel in localizedLabels {
|
||||
if let found = findElementWithVerticalSearch(
|
||||
app: app,
|
||||
label: localizedLabel,
|
||||
timeoutPerAttempt: 0.4,
|
||||
swipeCount: 8
|
||||
) {
|
||||
if found.isHittable {
|
||||
found.tap()
|
||||
} else {
|
||||
found.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,20 +524,19 @@ final class AndromidaUITests: XCTestCase {
|
||||
private func openHistoryDayDetailWithData(app: XCUIApplication) {
|
||||
openTab(app: app, label: "History", index: 3)
|
||||
|
||||
let nonZeroDay = app.buttons.matching(
|
||||
NSPredicate(format: "label CONTAINS[c] %@ AND NOT (label CONTAINS[c] %@)", "percent complete", "0 percent complete")
|
||||
).firstMatch
|
||||
|
||||
if nonZeroDay.waitForExistence(timeout: 8) {
|
||||
nonZeroDay.tap()
|
||||
// Locale-agnostic: prefer a day-like button with digits in its label.
|
||||
let dayPredicate = NSPredicate(format: "label MATCHES %@", ".*[0-9].*")
|
||||
let dayButton = app.buttons.matching(dayPredicate).firstMatch
|
||||
if dayButton.waitForExistence(timeout: 4) {
|
||||
dayButton.tap()
|
||||
} else {
|
||||
let anyDay = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "percent complete")).firstMatch
|
||||
if anyDay.waitForExistence(timeout: 8) {
|
||||
anyDay.tap()
|
||||
let buttons = app.buttons.allElementsBoundByIndex
|
||||
if let fallback = buttons.first(where: { $0.exists && $0.isHittable }) {
|
||||
fallback.tap()
|
||||
}
|
||||
}
|
||||
|
||||
_ = app.buttons["Done"].waitForExistence(timeout: 8)
|
||||
_ = firstExistingDoneButton(app: app, timeout: 2)
|
||||
}
|
||||
|
||||
private func tapFirstAvailableElementIfPresent(
|
||||
|
||||
BIN
Screenshots/es-MX/iPad-13/01-today-focus-portrait.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Screenshots/es-MX/iPad-13/02-ritual-arcs-portrait.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
Screenshots/es-MX/iPad-13/03-insights-trends-portrait.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
Screenshots/es-MX/iPad-13/04-history-calendar-portrait.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
Screenshots/es-MX/iPad-13/05-history-day-detail-portrait.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
Screenshots/es-MX/iPhone-6.3/01-today-focus-portrait.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Screenshots/es-MX/iPhone-6.3/02-ritual-arcs-portrait.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Screenshots/es-MX/iPhone-6.3/03-insights-trends-portrait.png
Normal file
|
After Width: | Height: | Size: 991 KiB |
BIN
Screenshots/es-MX/iPhone-6.3/04-history-calendar-portrait.png
Normal file
|
After Width: | Height: | Size: 961 KiB |
BIN
Screenshots/es-MX/iPhone-6.3/05-history-day-detail-portrait.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1004 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 282 KiB |
BIN
Screenshots/es-MX/iPhone-6.9/01-today-focus-portrait.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Screenshots/es-MX/iPhone-6.9/02-ritual-arcs-portrait.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
Screenshots/es-MX/iPhone-6.9/03-insights-trends-portrait.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/es-MX/iPhone-6.9/04-history-calendar-portrait.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/es-MX/iPhone-6.9/05-history-day-detail-portrait.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
Screenshots/fr-CA/iPad-13/01-today-focus-portrait.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Screenshots/fr-CA/iPad-13/02-ritual-arcs-portrait.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Screenshots/fr-CA/iPad-13/03-insights-trends-portrait.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Screenshots/fr-CA/iPad-13/04-history-calendar-portrait.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Screenshots/fr-CA/iPad-13/05-history-day-detail-portrait.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
Screenshots/fr-CA/iPhone-6.3/01-today-focus-portrait.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Screenshots/fr-CA/iPhone-6.3/02-ritual-arcs-portrait.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Screenshots/fr-CA/iPhone-6.3/03-insights-trends-portrait.png
Normal file
|
After Width: | Height: | Size: 950 KiB |
BIN
Screenshots/fr-CA/iPhone-6.3/04-history-calendar-portrait.png
Normal file
|
After Width: | Height: | Size: 963 KiB |
BIN
Screenshots/fr-CA/iPhone-6.3/05-history-day-detail-portrait.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 998 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 288 KiB |
BIN
Screenshots/fr-CA/iPhone-6.9/01-today-focus-portrait.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Screenshots/fr-CA/iPhone-6.9/02-ritual-arcs-portrait.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
Screenshots/fr-CA/iPhone-6.9/03-insights-trends-portrait.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/fr-CA/iPhone-6.9/04-history-calendar-portrait.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/fr-CA/iPhone-6.9/05-history-day-detail-portrait.png
Normal file
|
After Width: | Height: | Size: 306 KiB |