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
24 changes: 15 additions & 9 deletions ios/HouseScan/UI/Copy/ScanCopy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -487,15 +487,15 @@ enum ScanCopy {
static func reply(for step: GuidanceStep) -> (title: String, hint: String)? {
switch step {
case .aimAtGround, .aimAtWall:
(title: "Skip this spot", hint: "An installer will look at it instead.")
(title: "Skip this spot", hint: "An installer would need to look at it instead.")
case .tiltUp:
(title: "Skip this", hint: "Skips the view above this part of the wall. An installer will look at it instead.")
(title: "Skip this", hint: "Skips the view above this part of the wall. An installer would need to look at it instead.")
case .walk:
(title: "Can't get there", hint: "Ends the wall at the dashed line on the map. An installer will look at what's past it.")
(title: "Can't get there", hint: "Ends the wall at the dashed line on the map. An installer would need to look at what's past it.")
case .markNextWall:
(title: "Can't get there", hint: "Skips this part of the wall. An installer will look at it instead.")
(title: "Can't get there", hint: "Skips this part of the wall. An installer would need to look at it instead.")
case .seeBehind:
(title: cannotSeeBehind, hint: "Skips the part behind it. An installer will look at it instead.")
(title: cannotSeeBehind, hint: "Skips the part behind it. An installer would need to look at it instead.")
case .findMeter, .aimAtWallForMeter, .holdOnMeter, .markEnd, .stepBack, .walkComplete, .gap:
nil
}
Expand Down Expand Up @@ -598,7 +598,7 @@ enum ScanCopy {
switch answer {
case .fits: "A battery fits here"
case .oneMoreLook: "One more look"
case .installer: "An installer will confirm"
case .installer: "Needs an installer's review"
case .notHere: "Not on this wall"
}
}
Expand Down Expand Up @@ -667,8 +667,10 @@ enum ScanCopy {
static let wallNotMeasuredDetail = "The scan stopped before you walked along the wall on either side of your meter, so we can't tell where a battery would fit. Scan again and walk a few steps each way."
static let scanAgain = "Scan again"

static let installerConfirms = "An installer confirms this on site."
static let rulesNotFinal = "The placement rules aren't final yet, so an installer reviews every result for now."
/// Shown on every result, with or without a spot ("Not on this wall" has none), so it names
/// no spot.
static let installerConfirms = "Before any battery goes in, an installer has to confirm where it goes on site."
static let rulesNotFinal = "The placement rules aren't final yet, so every result needs an installer's review for now."
// The server's result covers where the battery goes, not the panel itself.
static let panelReview = "Your electrical panel still needs an electrician's review. This scan only covers where the battery can go."

Expand All @@ -690,8 +692,12 @@ enum ScanCopy {
static let shareScan = "Share scan"
static let shareScanContents = "Your photos and measurements, for the House Scan team"

/// A check or a view no photo can settle. It states what the result needs, never that a
/// review was sent: the app only shows the server's answer and contacts nobody.
static let needsInstaller = "Needs an installer to check"

static func unsureNote(_ row: CheckRow) -> String {
row.needsPerson ? "An installer will check this" : "One more photo would settle this"
row.needsPerson ? Self.needsInstaller : "One more photo would settle this"
}

/// "Measured 3 ft 2 in. The rule is at least 3 ft, and the measurement can be off by about 4 in."
Expand Down
6 changes: 3 additions & 3 deletions ios/HouseScan/UI/Copy/SpotCheckCopy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ extension ScanCopy {
static let spotClear = "It's clear"
static let spotSomethingThere = "Something's there"
static let spotClearHint = "Shows your result."
static let spotSomethingThereHint = "Leaves this area out of your scan and checks your wall again. An installer will look at it."
static let spotSomethingThereHint = "Leaves this area out of your scan and checks your wall again. An installer would need to look at it."

static func spotAnswered(_ answer: SpotCheckAnswer) -> Instruction {
switch answer {
case .clear: Instruction(title: "Thanks, it's clear", detail: "Showing your result.")
case .somethingThere: Instruction(title: "Thanks, we'll leave that area out", detail: "Checking your wall again. An installer will look at what's there.")
case .somethingThere: Instruction(title: "Thanks, we'll leave that area out", detail: "Checking your wall again. An installer would need to look at what's there.")
}
}

/// Said on the result when the homeowner said something stands where its spot is.
static let spotRefused = "You said something stands where this spot is. Your scan leaves that area out, and an installer will check it."
static let spotRefused = "You said something stands where this spot is. Your scan leaves that area out, and an installer would need to check it."

/// "From 3 ft to 8 ft 6 in right of your meter": where the area runs. `spoken` spells the
/// units out for VoiceOver.
Expand Down
2 changes: 1 addition & 1 deletion ios/HouseScan/UI/Preview/DemoEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1135,7 +1135,7 @@ final class DemoEngine: ScanActions {
/// The offline sample result. Placeholder rules, so the decision is manual review.
static let reviewSample = ResultPresentation(
decision: .manualReview,
summary: "A spot 3 ft right of your meter fits, but it's close to a window, so an installer will confirm it.",
summary: "A spot 3 ft right of your meter fits, but it's close to a window, so it needs an installer's review.",
policyApproved: false,
spot: BatterySpot(span: 0.55...1.34, depth: 0.56, height: 1.1, offsetFromWall: 0.03),
cableRoute: [SIMD2(0.16, 1.45), SIMD2(0.95, 1.45), SIMD2(0.95, 1.1)],
Expand Down
8 changes: 4 additions & 4 deletions ios/HouseScan/UI/Screens/GapRequestScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ struct GapRequestScreen: View {
Label("Show my result", systemImage: "checkmark.circle")
}
.buttonStyle(.secondary)
.accessibilityHint("Stops asking for views and shows your result. An installer will look at the parts you skip.")
.accessibilityHint("Stops asking for views and shows your result. An installer would need to look at the parts you skip.")
.accessibilityIdentifier("action.showResult")
.transition(.opacity)
}
Expand All @@ -119,9 +119,9 @@ struct GapRequestScreen: View {
/// so the hint doesn't promise the result; "Show my result" does.
private var skipHint: String {
switch followUps {
case 0: "Skips this view. An installer will look at this part instead."
case 1: "Skips this view and checks your scan again. An installer will look at this part instead."
default: "Skips this view and goes on to the next one. An installer will look at this part instead."
case 0: "Skips this view. An installer would need to look at this part instead."
case 1: "Skips this view and checks your scan again. An installer would need to look at this part instead."
default: "Skips this view and goes on to the next one. An installer would need to look at this part instead."
}
}

Expand Down
2 changes: 1 addition & 1 deletion ios/HouseScan/UI/Screens/MeterCloseUpScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ struct MeterCloseUpScreen: View {
if offerSkip {
Button("Can't get a clear shot") { actions.skipCloseUp() }
.buttonStyle(.secondaryProminent)
.accessibilityHint("Skips the close-up. An installer will read the meter instead.")
.accessibilityHint("Skips the close-up. An installer would need to read the meter instead.")
.accessibilityIdentifier("action.skipCloseUp")
.transition(.identity)
}
Expand Down
4 changes: 2 additions & 2 deletions ios/HouseScan/UI/Screens/ResultARScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ struct ResultARScreen: View {
guard result.spot != nil else { return "The cable run and clearances, drawn on your wall" }
return result.spotIsClean
? "The battery, drawn on your wall at its spot"
: "An outline of the spot an installer will check, drawn on your wall"
: "An outline of the spot an installer needs to check, drawn on your wall"
}

private var instruction: Instruction {
Expand All @@ -71,7 +71,7 @@ struct ResultARScreen: View {
// check says so, as the result screen does.
let title = result.decision == .pass && result.policyApproved && result.spotIsClean
? "Your battery could go here"
: "The spot an installer will check"
: "The spot an installer needs to check"
let placement = ScanCopy.placement(result)
guard !result.isSample else {
// A sample spot drawn on the homeowner's real wall must not pass for their result.
Expand Down
2 changes: 1 addition & 1 deletion ios/HouseScan/UI/Screens/ResultScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ private struct MissingList: View {
.buttonStyle(TextActionStyle())
.accessibilityIdentifier("action.captureMissing")
} else {
Label("An installer will check this", systemImage: "person.fill")
Label(ScanCopy.needsInstaller, systemImage: "person.fill")
.font(.footnote.weight(.semibold))
.foregroundStyle(Palette.muted)
}
Expand Down
20 changes: 19 additions & 1 deletion ios/HouseScanUITests/FullFlowUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,25 @@ final class FullFlowUITests: XCTestCase {
@MainActor
func testFullFlowFromReplay() throws {
let replay = Self.environment["HOUSESCAN_REPLAY"].flatMap { $0.isEmpty ? nil : $0 } ?? Self.fixture
try runFlow(replay: replay)
// A real server's answer can be any decision; the bundled sample is always a manual review.
let sample = Self.environment["HOUSESCAN_SERVER_URL"]?.isEmpty ?? true
try runFlow(replay: replay) { app, phase in
if sample, phase == "result" { Self.assertInstallerReviewCopy(app) }
}
}

/// The sample result needs a person to settle it (`SampleResult.json`). The app only shows
/// the answer and contacts nobody, so its words say what the result needs, never that an
/// installer will confirm or review it.
@MainActor
private static func assertInstallerReviewCopy(_ app: XCUIApplication) {
func label(_ identifier: String) -> String? {
let element = app.descendants(matching: .any)[identifier].firstMatch
return element.exists ? element.label : nil
}
XCTAssertEqual(label("result.headline"), "Needs an installer's review")
XCTAssertEqual(label("result.installerConfirms"), "Before any battery goes in, an installer has to confirm where it goes on site.")
XCTAssertEqual(label("result.rulesNotFinal"), "The placement rules aren't final yet, so every result needs an installer's review for now.")
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

/// The flow from the LiDAR fixture. Depth must show the bin in front of the wall: the wall map
Expand Down
43 changes: 43 additions & 0 deletions ios/HouseScanUITests/InstallerReviewUITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import XCTest

/// The result says what it needs from a person, never that one was contacted: the app only shows
/// the server's answer. Held still on the demo's review sample (`DemoEngine.reviewSample`), whose
/// window check needs a person and whose ground check needs a photo. Its headline is "One more
/// look", since a view can still be captured; `FullFlowUITests.testFullFlowFromReplay` checks the
/// installer headline on the bundled sample.
final class InstallerReviewUITests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}

@MainActor
func testTheResultSaysAnInstallerIsNeededNotArranged() throws {
let app = XCUIApplication()
app.launchArguments = ["-practiceMeter", "NO", "-uiDemo", "-uiDemoFreeze", "-uiDemoPhase", "result"]
app.launch()
XCTAssertTrue(element(app, "screen.result").waitForExistence(timeout: 15))
XCTAssertEqual(element(app, "result.installerConfirms").label, "Before any battery goes in, an installer has to confirm where it goes on site.")

// The full list of checks sits under Details, below the footnotes.
let details = element(app, "result.details")
XCTAssertTrue(details.waitForExistence(timeout: 10))
for _ in 0..<4 where !details.isHittable { app.swipeUp() }
details.tap()
let window = element(app, "detail.check.window")
let ground = element(app, "detail.check.ground")
XCTAssertTrue(window.waitForExistence(timeout: 10), "the window check never appeared under Details")
// The two unsure checks keep their difference: a person settles one, a photo the other.
XCTAssertTrue(value(window).contains("Needs an installer to check"), "window check reads: \(value(window))")
XCTAssertTrue(value(ground).contains("One more photo would settle this"), "ground check reads: \(value(ground))")
}

@MainActor
private func element(_ app: XCUIApplication, _ identifier: String) -> XCUIElement {
app.descendants(matching: .any)[identifier].firstMatch
}

@MainActor
private func value(_ element: XCUIElement) -> String {
element.value as? String ?? ""
}
}
Loading