diff --git a/OwnFrameUITests/PurchaseGateUITests.swift b/OwnFrameUITests/PurchaseGateUITests.swift index e0279048..ae7c1163 100644 --- a/OwnFrameUITests/PurchaseGateUITests.swift +++ b/OwnFrameUITests/PurchaseGateUITests.swift @@ -18,7 +18,9 @@ // running slideshow with zero purchase UI at every step. // // Assertions 3, 5 and 6 (part-ownership visibility, store-unavailable, pre-gate broker -// config) are separate files; assertion 7 is explicitly not XCUITest. +// config) are separate files; assertion 7 is explicitly not XCUITest. Assertion 3's +// `ProductID.uiSlug` parenthetical (PR #40 post-merge review gap — `unlock.price.supporter` / +// `unlock.buy.supporter` were unasserted) is covered here. // import XCTest @@ -102,6 +104,36 @@ final class PurchaseGateUITests: XCTestCase { "the broker control-locked row must open unlock.screen.supporter") } + // MARK: - ProductID.uiSlug coverage — price + buy identifiers on the unlock screen + + /// Closes the gap the PR #40 post-merge review found (recorded in contracts/uitest-seams.md + /// assertion 3's parenthetical): `ProductID.uiSlug` feeds `unlock.price.supporter` and + /// `unlock.buy.supporter`, but no XCUITest ever asserted either existed. Opens the one + /// unlock screen from a locked row under `none` entitlements — `launchIntoSettings` leaves + /// `--uitest-store=` unset, which `PurchaseUITestSeams.storeBehavior` defaults to `.stub`, so + /// the screen has a real stub product ("$1.00") to price. + @MainActor + func testUnlockScreenShowsSupporterPriceAndBuyIdentifiers() throws { + let app = launchIntoSettings(entitlements: "none") + + let kenBurnsRow = element(app, "settings.row.kenburns.locked") + XCTAssertTrue(scrollToElement(kenBurnsRow, in: app), "kenburns locked row must be present") + kenBurnsRow.tap() + + XCTAssertTrue(element(app, "unlock.screen.supporter").waitForExistence(timeout: 5), + "the locked row must open unlock.screen.supporter") + + let price = element(app, "unlock.price.supporter") + XCTAssertTrue(price.waitForExistence(timeout: 5), + "unlock.price.supporter must exist once the stub store's products load") + XCTAssertFalse(price.label.isEmpty, + "unlock.price.supporter must carry a non-empty price label") + + let buy = element(app, "unlock.buy.supporter") + XCTAssertTrue(buy.waitForExistence(timeout: 5), "unlock.buy.supporter must exist") + XCTAssertTrue(buy.isHittable, "unlock.buy.supporter must be hittable, not merely present") + } + // MARK: - Assertion 6 — pre-gate broker config degrades gracefully (US5 / SC-1100-06) /// A frame configured before the gate keeps its broker settings, and telemetry is free — so diff --git a/specs/1100-purchase-gate/contracts/uitest-seams.md b/specs/1100-purchase-gate/contracts/uitest-seams.md index a24064a0..d4359397 100644 --- a/specs/1100-purchase-gate/contracts/uitest-seams.md +++ b/specs/1100-purchase-gate/contracts/uitest-seams.md @@ -43,8 +43,10 @@ against the stub source, hermetic, no server). `unlock.screen.supporter` — there is never a second product or screen to choose (FR-1100-04). Under `supporter`/`all` those same rows are unlocked and the screen is unreachable — partial ownership cannot occur, so there is no per-tier visibility state left to exercise. - (`unlock.price.supporter` / `unlock.buy.supporter` exist on that screen but are not - independently asserted by any XCUITest today — `ProductID.uiSlug` itself is untested.) + (`unlock.price.supporter` / `unlock.buy.supporter` are asserted by + `PurchaseGateUITests.testUnlockScreenShowsSupporterPriceAndBuyIdentifiers` — non-empty + price label, hittable buy button — closing the `ProductID.uiSlug` gap the PR #40 + post-merge review documented here.) 4. `all` (== `supporter`): no locked rows, no `unlock.` entry points except Restore + tips in settings. 5. `--uitest-store=unavailable`: unlock screen shows `unlock.unavailable`, zero price labels