From a9921b1c10c37b1904e452f35c07dc36d9fbe38c Mon Sep 17 00:00:00 2001 From: Danny Dover Date: Sun, 8 Nov 2026 21:42:22 -0600 Subject: [PATCH] Retain normal-notebook smoke without Friend toggle --- Development/Kin13LayeredValidation.md | 2 +- Development/QA-NextRelease.md | 10 ++++++ KinUITests/KinUITests.swift | 49 +++++++++++++++++++++++++++ scripts/ci-test-shards.py | 2 +- scripts/kin13-coverage-map.md | 6 ++-- scripts/tests/test_ci_test_shards.py | 9 +++++ 6 files changed, 74 insertions(+), 4 deletions(-) diff --git a/Development/Kin13LayeredValidation.md b/Development/Kin13LayeredValidation.md index 6174ee4..a22f77b 100644 --- a/Development/Kin13LayeredValidation.md +++ b/Development/Kin13LayeredValidation.md @@ -18,7 +18,7 @@ To run the broad suite for investigation or before release, dispatch `Manual bro | Behavior | Proposed blocking evidence | Still requires broader UI or manual review | |---|---|---| -| Normal app launch, create, save and cold relaunch | `KinUITests.testNormalColdLaunchOpensNotebookAndPreservesSavedPerson` | Other onboarding, error and editing paths | +| Normal app launch, create, save and cold relaunch | `KinUITests.testNormalColdLaunchCreatesAndPersistsUncategorizedPerson` | The real Friend-on switch and its persistence, plus other onboarding, error and editing paths | | Link, edit and reopen a relationship | `SmokeUITests.testRelationshipEditPersistsAfterRelaunch`; `PersonTests` and `SequentialImportRestartTests` | Other relationship roles, reversal, cancellation and accessibility permutations | | Filters, birthdays, identity, cancellation, saving and reopening | Complete Core and iOS `KinTests` target, including `PeopleFilterStateTests`, `FilterStoreSequenceTests`, `ExistingContactBirthPreservationTests`, and store/relationship tests | The full range of rendered navigation, gestures and accessibility behavior | | Contact and photo rules and persistence | `ContactPickerTests`, `ExistingContactBirthPreservationTests`, `ContactPhotoReviewTests`, `NewPersonImportTests`, `SequentialImportRestartTests`, and `PhotoProcessorTests` | Real system-picker presentation, selection, permission transitions and native payload delivery | diff --git a/Development/QA-NextRelease.md b/Development/QA-NextRelease.md index a2934bb..c240de3 100644 --- a/Development/QA-NextRelease.md +++ b/Development/QA-NextRelease.md @@ -3,6 +3,16 @@ Use synthetic people and photos only. Preserve existing notebook data; do not reset, uninstall, or use Delete All as part of this plan. +## Required Kin Dev 1.3 Friend check + +On the installed Kin Dev 1.3 candidate, create a synthetic person, switch +Friend from off to on once, save, and confirm that the person's profile and +People row appear. Quit and relaunch Kin Dev, reopen the person and edit view, +and verify that Friend remains on. Record the build and result. Required CI's +normal-notebook smoke creates and persists an uncategorized person; it no longer +proves the real Friend-on interaction or its persistence. Kin Dev QA does not +validate production Release signing or an upgrade of existing production data. + On October 27, the owner reported steps 1–11 passed on installed Kin Dev 1.2.0 (6), including both the largest-text and VoiceOver checklist steps. This is owner-reported physical acceptance of that candidate, with three reported UX diff --git a/KinUITests/KinUITests.swift b/KinUITests/KinUITests.swift index 3f65ab1..b28f27f 100644 --- a/KinUITests/KinUITests.swift +++ b/KinUITests/KinUITests.swift @@ -17,6 +17,55 @@ final class KinUITests: XCTestCase { } print("KIN_CI_AUTH \(label)") } + @MainActor func testNormalColdLaunchCreatesAndPersistsUncategorizedPerson() throws { + #if targetEnvironment(simulator) + // Exercise the actual normal notebook, without fixture or test-store arguments. + let app = XCUIApplication() + app.launchArguments = [] + addTeardownBlock { app.terminate() } + app.launch() + XCTAssertTrue(app.buttons["Add person"].waitForExistence(timeout: 15)) + XCTAssertFalse(app.staticTexts["Notebook locked"].exists) + let name = "Synthetic Cold Launch " + String(UUID().uuidString.prefix(8)) + let add = app.buttons.matching(identifier: "Add person") + guard requireTestState(app, "Add person action ready", { + add.count == 1 && add.element.isHittable + }) else { return } + add.element.tap() + let editor = app.navigationBars["Add person"] + let firstName = app.textFields["First name (optional)"] + guard requireTestState(app, "New person editor ready", { + editor.exists && editor.isHittable && firstName.isHittable + }) else { return } + firstName.tap(); firstName.typeText(name) + guard requireTestState(app, "New person name entered", { + firstName.value as? String == name + }) else { return } + let save = editor.buttons.matching(identifier: "Save") + guard requireTestState(app, "Save enabled", { + save.count == 1 && save.element.isEnabled && save.element.isHittable + }) else { return } + save.element.tap() + guard requireTestState(app, "Direct save opens saved profile", { + app.navigationBars[name].exists && !editor.exists + }) else { return } + app.navigationBars.buttons["People"].tap() + guard requireTestState(app, "Saved person returned to exact index row", { + !editor.exists && app.staticTexts[name].exists + && app.cells.containing(.staticText, identifier: name).count == 1 + }) else { return } + app.terminate(); app.launch() + XCTAssertTrue(app.staticTexts[name].waitForExistence(timeout: 15)) + XCTAssertFalse(app.staticTexts["Notebook locked"].exists) + XCTAssertEqual(app.cells.containing(.staticText, identifier: name).count, 1) + app.staticTexts[name].tap() + XCTAssertTrue(app.navigationBars[name].waitForExistence(timeout: 5)) + #else + throw XCTSkip("Normal notebook startup regression requires an isolated simulator.") + #endif + } + + // Retained in the manual broad UI suite: includes the real Friend toggle. @MainActor func testNormalColdLaunchOpensNotebookAndPreservesSavedPerson() throws { #if targetEnvironment(simulator) // Run only in a fresh isolated simulator: exercise the normal notebook, diff --git a/scripts/ci-test-shards.py b/scripts/ci-test-shards.py index dda1ada..5d682d3 100644 --- a/scripts/ci-test-shards.py +++ b/scripts/ci-test-shards.py @@ -9,7 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] GROUPS = ('unit', 'ui-0', 'ui-1', 'ui-2', 'ui-3') SMOKE_IDS = ( - 'KinUITests/KinUITests/testNormalColdLaunchOpensNotebookAndPreservesSavedPerson', + 'KinUITests/KinUITests/testNormalColdLaunchCreatesAndPersistsUncategorizedPerson', 'KinUITests/SmokeUITests/testEmptyPeopleScreenAtNormalText', 'KinUITests/SmokeUITests/testEmptyPeopleScreenAtLargestText', 'KinUITests/SmokeUITests/testRelationshipEditPersistsAfterRelaunch', diff --git a/scripts/kin13-coverage-map.md b/scripts/kin13-coverage-map.md index a67673f..2bd66a5 100644 --- a/scripts/kin13-coverage-map.md +++ b/scripts/kin13-coverage-map.md @@ -161,7 +161,8 @@ No accessibility, visible-copy, or layout test is retired by this map. | Retained UI test | Assertions that remain at the UI layer | |---|---| -| `testNormalColdLaunchOpensNotebookAndPreservesSavedPerson` | A normal isolated-simulator launch without test-store flags opens the notebook, saves a synthetic person, and shows that person and profile after cold relaunch without a locked-notebook message. | +| `testNormalColdLaunchCreatesAndPersistsUncategorizedPerson` | Required smoke: a normal isolated-simulator launch without test-store flags saves an uncategorized synthetic person, confirms the row and profile, and confirms persistence after cold relaunch without a locked-notebook message. | +| `testNormalColdLaunchOpensNotebookAndPreservesSavedPerson` | Manual broad UI: retains the normal-notebook creation journey with the real Friend-on interaction. | | `testPrivacyPolicyIsFinalAboutSectionAtNormalAndLargestText` | At both sizes the policy link is hittable, its bottom is within the viewport, it is at least 44 points high and follows the developer section; synthetic geometry and screenshots are retained. | | `testIllustratedEmptyStateMenuAndSearch` and `testUnlistedPeopleMenuReturnAndRelaunch` | Together retain empty-state copy and first-person action, menu/About navigation, unlisted-person recovery through All people, no-match search, and empty main-list state after relaunch. The latter starts from an isolated named Friend fixture. | | `testBlankNameEditsCancelAndLaterNamingPreserveEntry` | Cancel retains a named entry; saving whitespace yields the stable unnamed entry; naming it after relaunch opens the renamed profile. | @@ -248,7 +249,8 @@ Layer key: **native** = real platform integration; **app UI** = navigation, inte | `IndexMembershipUITests.testPreviewClearAllPeopleSearchRestoresPeopleWithoutChangingRecords` | app UI, integration, unit | Passed in run 41245990817; shared assertions move to the original test name. | | `IndexMembershipUITests.testClearAllPeopleSearchRestoresPeopleWithoutChangingRecords` | app UI, integration, unit | Required; uses the validated in-memory fixture and retains all shared assertions. The migrated head needs validation. | | `IndexMembershipUITests.testClearSearchAndFiltersOnEmptyNotebookAtLargestText` | app UI, integration, unit | Retain; assertions mapped above. | -| `KinUITests.testNormalColdLaunchOpensNotebookAndPreservesSavedPerson` | app UI, integration | Retain; assertions mapped above. | +| `KinUITests.testNormalColdLaunchCreatesAndPersistsUncategorizedPerson` | app UI, integration | Required normal-notebook smoke; assertions mapped above. | +| `KinUITests.testNormalColdLaunchOpensNotebookAndPreservesSavedPerson` | app UI, integration | Retained in manual broad UI; exercises the actual Friend-on interaction. | | `SmokeUITests.testRelationshipEditPersistsAfterRelaunch` | app UI, integration | Additive candidate: a real relationship is linked, edited, saved and verified after relaunch in an isolated synthetic notebook. The full relationship UI journeys remain required during comparison. | | `SmokeUITests.testEmptyPeopleScreenAtNormalText` | app UI, rendered layout | Captures the empty People screen at normal text after verifying its navigation and heading; screenshots do not prove clipping or VoiceOver behavior. | | `SmokeUITests.testEmptyPeopleScreenAtLargestText` | app UI, rendered layout | Captures the empty People screen at largest text after verifying its navigation and heading; device review remains necessary for visual and accessibility quality. | diff --git a/scripts/tests/test_ci_test_shards.py b/scripts/tests/test_ci_test_shards.py index 54ef6cf..4ee1652 100644 --- a/scripts/tests/test_ci_test_shards.py +++ b/scripts/tests/test_ci_test_shards.py @@ -62,6 +62,15 @@ def test_smoke_is_exact_subset_and_missing_identity_fails_closed(self): with self.assertRaisesRegex(ValueError, 'Smoke identities'): shards.selections('smoke') + def test_normal_notebook_smoke_and_friend_journey_are_both_selected(self): + narrow = 'KinUITests/KinUITests/testNormalColdLaunchCreatesAndPersistsUncategorizedPerson' + friend = 'KinUITests/KinUITests/testNormalColdLaunchOpensNotebookAndPreservesSavedPerson' + self.assertIn(narrow, shards.selections('smoke')) + self.assertNotIn(friend, shards.selections('smoke')) + broad = [item for group in shards.GROUPS[1:] for item in shards.selections(group)] + self.assertEqual(broad.count(narrow), 1) + self.assertEqual(broad.count(friend), 1) + def test_invalid_shard_fails_closed(self): with self.assertRaises(ValueError): shards.selections('ui-9')