Coverage: iOS ViewModel DI seam + populated-state snapshots
6 user-facing ViewModels now accept optional `dataManager: DataManagerObservable = .shared` init param — production call-sites unchanged; tests inject fixture-backed observables. Refactored: ResidenceViewModel, TaskViewModel, ContractorViewModel, DocumentViewModel, ProfileViewModel, LoginViewModel. DataManagerObservable gains test-only init(observeSharedDataManager:) + convenience init(kotlin: IDataManager). SnapshotGalleryTests.setUp() resets .shared to FixtureDataManager.empty() per test; populated tests call seedPopulated() to copy every StateFlow from FixtureDataManager.populated() onto .shared synchronously. 15 populated surfaces × 2 modes = 30 new PNGs. iOS goldens: 58 → 88. 44 SnapshotGalleryTests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -2,26 +2,30 @@
|
||||
// SnapshotGalleryTests.swift
|
||||
// HoneyDueTests
|
||||
//
|
||||
// P3 — iOS parity gallery. Records baseline PNGs for primary SwiftUI screens
|
||||
// across {empty-state} × {light, dark}. Complements the Android Roborazzi
|
||||
// gallery (P2); both platforms consume the same Kotlin FixtureDataManager
|
||||
// fixtures (P1), so any layout divergence between Android + iOS renders of
|
||||
// the same screen is a real parity bug — not a test-data mismatch.
|
||||
// P3/P5 — iOS parity gallery. Records baseline PNGs for primary SwiftUI
|
||||
// screens across {empty, populated} × {light, dark}. Complements the
|
||||
// Android Roborazzi gallery; both platforms consume the same Kotlin
|
||||
// `FixtureDataManager` fixtures, so any layout divergence between Android
|
||||
// and iOS renders of the same screen is a real parity bug — not a test
|
||||
// data mismatch.
|
||||
//
|
||||
// Current state coverage
|
||||
// ----------------------
|
||||
// State coverage
|
||||
// --------------
|
||||
// * Empty (signed-in user with no residences / tasks / docs / contractors)
|
||||
// is captured for every screen below via the default
|
||||
// `DataManagerObservable.shared`, which has no data loaded when tests run
|
||||
// because no login has occurred in the test host.
|
||||
// * Populated state is BLOCKED on a follow-up ViewModel-injection refactor:
|
||||
// current SwiftUI screens instantiate their ViewModels via
|
||||
// `@StateObject private var viewModel = FooViewModel()`, and those
|
||||
// ViewModels read directly from `DataManagerObservable.shared` rather
|
||||
// than an injected `IDataManager`. Swapping the singleton is unsafe in
|
||||
// parallel tests. Follow-up PR: add an optional `dataManager:` init param
|
||||
// to each `*ViewModel.swift` and thread it from here via
|
||||
// `.environment(\.dataManager, ...)`.
|
||||
// is captured by clearing `DataManagerObservable.shared`'s @Published
|
||||
// caches before the view is instantiated.
|
||||
// * Populated is captured by synchronously seeding those caches from
|
||||
// `FixtureDataManager.populated()` via `DataManagerObservable(kotlin:)`
|
||||
// and copying its values onto `.shared`. The seeded values persist
|
||||
// because the production Kotlin `DataManager.shared` never emits during
|
||||
// tests (no API calls are made), so the continuous observation tasks
|
||||
// never overwrite our seed.
|
||||
// * Each user-facing ViewModel gained a `dataManager:` init seam in the
|
||||
// same commit so follow-up tests can inject a dedicated instance
|
||||
// instead of mutating `.shared`. For the gallery we pick the simpler
|
||||
// shared-seed path because the existing views use `@StateObject = VM()`
|
||||
// without an init param, and refactoring every call-site to thread a
|
||||
// VM through is out of scope.
|
||||
//
|
||||
// Recording goldens
|
||||
// -----------------
|
||||
@@ -30,11 +34,10 @@
|
||||
// xcodebuild env, deletes the old `__Snapshots__/SnapshotGalleryTests`
|
||||
// directory, runs the target, then invokes the shared PNG optimizer.
|
||||
//
|
||||
// Manual override: set the `SNAPSHOT_TESTING_RECORD` env var to `1` in
|
||||
// the Xcode scheme's Test action (Edit Scheme → Test → Arguments →
|
||||
// Environment Variables) and re-run the test target. CI fails the
|
||||
// build if a screen diverges from its golden by more than the
|
||||
// precision threshold.
|
||||
// Manual override: set `SNAPSHOT_TESTING_RECORD=1` in the Xcode scheme's
|
||||
// Test action (Edit Scheme → Test → Arguments → Environment Variables)
|
||||
// and re-run the test target. CI fails the build if a screen diverges
|
||||
// from its golden by more than the precision threshold.
|
||||
//
|
||||
// Rendering scale
|
||||
// ---------------
|
||||
@@ -71,6 +74,119 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Default to empty state before every test. Each populated-state
|
||||
// test explicitly calls `seedPopulated()` at the top of its body.
|
||||
seedEmpty()
|
||||
}
|
||||
|
||||
// MARK: - Fixture seeding
|
||||
|
||||
/// Reset `DataManagerObservable.shared` to the empty-fixture baseline.
|
||||
/// ViewModels that `.sink` on `.shared`'s publishers during init will
|
||||
/// receive these values immediately and render the empty-state UI.
|
||||
private func seedEmpty() {
|
||||
copyFixture(FixtureDataManager.shared.empty(), into: DataManagerObservable.shared)
|
||||
}
|
||||
|
||||
/// Seed `DataManagerObservable.shared` from `FixtureDataManager.populated()`
|
||||
/// so the next view instantiated in the test picks up fully-populated
|
||||
/// caches via its Combine subscription.
|
||||
private func seedPopulated() {
|
||||
copyFixture(FixtureDataManager.shared.populated(), into: DataManagerObservable.shared)
|
||||
}
|
||||
|
||||
/// Synchronously copy every StateFlow value from an `IDataManager`
|
||||
/// fixture onto a `DataManagerObservable`'s `@Published` properties.
|
||||
/// Mirrors the `init(kotlin:)` seed path but targets an existing
|
||||
/// instance so we can reuse `.shared` (required because views
|
||||
/// instantiate their ViewModels with the default `.shared` argument).
|
||||
private func copyFixture(_ fixture: IDataManager, into observable: DataManagerObservable) {
|
||||
observable.currentUser = fixture.currentUser.value
|
||||
observable.isAuthenticated = fixture.currentUser.value != nil
|
||||
|
||||
observable.residences = fixture.residences.value
|
||||
observable.myResidences = fixture.myResidences.value
|
||||
observable.totalSummary = fixture.totalSummary.value
|
||||
observable.residenceSummaries = mapInt(fixture.residenceSummaries.value)
|
||||
|
||||
observable.allTasks = fixture.allTasks.value
|
||||
observable.tasksByResidence = mapInt(fixture.tasksByResidence.value)
|
||||
|
||||
observable.documents = fixture.documents.value
|
||||
observable.documentsByResidence = mapIntArray(fixture.documentsByResidence.value)
|
||||
|
||||
observable.contractors = fixture.contractors.value
|
||||
|
||||
observable.subscription = fixture.subscription.value
|
||||
observable.upgradeTriggers = mapString(fixture.upgradeTriggers.value)
|
||||
observable.featureBenefits = fixture.featureBenefits.value
|
||||
observable.promotions = fixture.promotions.value
|
||||
|
||||
observable.residenceTypes = fixture.residenceTypes.value
|
||||
observable.taskFrequencies = fixture.taskFrequencies.value
|
||||
observable.taskPriorities = fixture.taskPriorities.value
|
||||
observable.taskCategories = fixture.taskCategories.value
|
||||
observable.contractorSpecialties = fixture.contractorSpecialties.value
|
||||
|
||||
observable.taskTemplates = fixture.taskTemplates.value
|
||||
observable.taskTemplatesGrouped = fixture.taskTemplatesGrouped.value
|
||||
|
||||
let hasLookups = !fixture.residenceTypes.value.isEmpty ||
|
||||
!fixture.taskPriorities.value.isEmpty ||
|
||||
!fixture.taskCategories.value.isEmpty
|
||||
observable.lookupsInitialized = hasLookups
|
||||
observable.isInitialized = hasLookups
|
||||
}
|
||||
|
||||
private func mapInt<V>(_ kotlinMap: Any?) -> [Int32: V] {
|
||||
guard let nsDict = kotlinMap as? NSDictionary else { return [:] }
|
||||
var result: [Int32: V] = [:]
|
||||
for key in nsDict.allKeys {
|
||||
guard let value = nsDict[key], let typed = value as? V else { continue }
|
||||
if let ki = key as? KotlinInt {
|
||||
result[ki.int32Value] = typed
|
||||
} else if let ns = key as? NSNumber {
|
||||
result[ns.int32Value] = typed
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func mapIntArray<V>(_ kotlinMap: Any?) -> [Int32: [V]] {
|
||||
guard let nsDict = kotlinMap as? NSDictionary else { return [:] }
|
||||
var result: [Int32: [V]] = [:]
|
||||
for key in nsDict.allKeys {
|
||||
guard let value = nsDict[key] else { continue }
|
||||
let typed: [V]
|
||||
if let arr = value as? [V] {
|
||||
typed = arr
|
||||
} else if let nsArr = value as? NSArray {
|
||||
typed = nsArr.compactMap { $0 as? V }
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if let ki = key as? KotlinInt {
|
||||
result[ki.int32Value] = typed
|
||||
} else if let ns = key as? NSNumber {
|
||||
result[ns.int32Value] = typed
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func mapString<V>(_ kotlinMap: Any?) -> [String: V] {
|
||||
if let direct = kotlinMap as? [String: V] { return direct }
|
||||
guard let nsDict = kotlinMap as? NSDictionary else { return [:] }
|
||||
var result: [String: V] = [:]
|
||||
for key in nsDict.allKeys {
|
||||
guard let s = key as? String, let v = nsDict[key] as? V else { continue }
|
||||
result[s] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Snapshot a SwiftUI view in both light + dark modes under a stable
|
||||
@@ -137,7 +253,7 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Auth flow
|
||||
// MARK: - Auth flow (empty-only; these screens have no backing data)
|
||||
|
||||
func test_login_empty() {
|
||||
snap("login_empty") {
|
||||
@@ -181,7 +297,7 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Onboarding
|
||||
// MARK: - Onboarding (empty-only; these screens are pre-data)
|
||||
|
||||
func test_onboarding_welcome_empty() {
|
||||
snap("onboarding_welcome_empty") {
|
||||
@@ -249,18 +365,39 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_residences_list_populated() {
|
||||
seedPopulated()
|
||||
snap("residences_list_populated") {
|
||||
NavigationStack { ResidencesListView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_residence_empty() {
|
||||
snap("add_residence_empty") {
|
||||
AddResidenceView(isPresented: .constant(true), onResidenceCreated: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_residence_populated() {
|
||||
seedPopulated()
|
||||
snap("add_residence_populated") {
|
||||
AddResidenceView(isPresented: .constant(true), onResidenceCreated: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func test_join_residence_empty() {
|
||||
snap("join_residence_empty") {
|
||||
JoinResidenceView(onJoined: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_join_residence_populated() {
|
||||
seedPopulated()
|
||||
snap("join_residence_populated") {
|
||||
JoinResidenceView(onJoined: {})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tasks
|
||||
|
||||
func test_all_tasks_empty() {
|
||||
@@ -269,18 +406,40 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_all_tasks_populated() {
|
||||
seedPopulated()
|
||||
snap("all_tasks_populated") {
|
||||
NavigationStack { AllTasksView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_task_empty() {
|
||||
snap("add_task_empty") {
|
||||
AddTaskView(residenceId: 1, isPresented: .constant(true))
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_task_populated() {
|
||||
seedPopulated()
|
||||
snap("add_task_populated") {
|
||||
AddTaskView(residenceId: 1, isPresented: .constant(true))
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_task_with_residence_empty() {
|
||||
snap("add_task_with_residence_empty") {
|
||||
AddTaskWithResidenceView(isPresented: .constant(true), residences: [])
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_task_with_residence_populated() {
|
||||
seedPopulated()
|
||||
let fixtureResidences = DataManagerObservable.shared.myResidences?.residences ?? []
|
||||
snap("add_task_with_residence_populated") {
|
||||
AddTaskWithResidenceView(isPresented: .constant(true), residences: fixtureResidences)
|
||||
}
|
||||
}
|
||||
|
||||
func test_task_suggestions_empty() {
|
||||
snap("task_suggestions_empty") {
|
||||
TaskSuggestionsView(
|
||||
@@ -290,12 +449,30 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_task_suggestions_populated() {
|
||||
seedPopulated()
|
||||
// TaskSuggestionsView accepts [TaskTemplate] directly; pulling the
|
||||
// first few templates from the populated fixture exercises the
|
||||
// same layout as production's "For You" tab.
|
||||
let templates = Array(DataManagerObservable.shared.taskTemplates.prefix(4))
|
||||
snap("task_suggestions_populated") {
|
||||
TaskSuggestionsView(suggestions: templates, onSelect: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
func test_task_templates_browser_empty() {
|
||||
snap("task_templates_browser_empty") {
|
||||
NavigationStack { TaskTemplatesBrowserView(onSelect: { _ in }) }
|
||||
}
|
||||
}
|
||||
|
||||
func test_task_templates_browser_populated() {
|
||||
seedPopulated()
|
||||
snap("task_templates_browser_populated") {
|
||||
NavigationStack { TaskTemplatesBrowserView(onSelect: { _ in }) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Contractor
|
||||
|
||||
func test_contractors_list_empty() {
|
||||
@@ -304,6 +481,13 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_contractors_list_populated() {
|
||||
seedPopulated()
|
||||
snap("contractors_list_populated") {
|
||||
NavigationStack { ContractorsListView() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Documents
|
||||
|
||||
func test_documents_warranties_empty() {
|
||||
@@ -312,6 +496,13 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_documents_warranties_populated() {
|
||||
seedPopulated()
|
||||
snap("documents_warranties_populated") {
|
||||
NavigationStack { DocumentsWarrantiesView(residenceId: nil) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Profile
|
||||
|
||||
func test_profile_tab_empty() {
|
||||
@@ -320,24 +511,52 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_profile_tab_populated() {
|
||||
seedPopulated()
|
||||
snap("profile_tab_populated") {
|
||||
NavigationStack { ProfileTabView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_profile_edit_empty() {
|
||||
snap("profile_edit_empty") {
|
||||
NavigationStack { ProfileView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_profile_edit_populated() {
|
||||
seedPopulated()
|
||||
snap("profile_edit_populated") {
|
||||
NavigationStack { ProfileView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_notification_preferences_empty() {
|
||||
snap("notification_preferences_empty") {
|
||||
NavigationStack { NotificationPreferencesView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_notification_preferences_populated() {
|
||||
seedPopulated()
|
||||
snap("notification_preferences_populated") {
|
||||
NavigationStack { NotificationPreferencesView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_theme_selection_empty() {
|
||||
snap("theme_selection_empty") {
|
||||
NavigationStack { ThemeSelectionView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_theme_selection_populated() {
|
||||
seedPopulated()
|
||||
snap("theme_selection_populated") {
|
||||
NavigationStack { ThemeSelectionView() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscription
|
||||
|
||||
func test_feature_comparison_empty() {
|
||||
@@ -346,6 +565,13 @@ final class SnapshotGalleryTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_feature_comparison_populated() {
|
||||
seedPopulated()
|
||||
snap("feature_comparison_populated") {
|
||||
FeatureComparisonView(isPresented: .constant(true))
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: UpgradeFeatureView is intentionally excluded from the parity
|
||||
// gallery. It reads `SubscriptionCacheWrapper.shared` and
|
||||
// `StoreKitManager.shared` on appear, both of which populate
|
||||
|
||||
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 329 KiB After Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 435 KiB After Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 344 KiB |
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 359 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 230 KiB |