diff --git a/apple/walkieware/Resources/Web/engine.mjs b/apple/walkieware/Resources/Web/engine.mjs index cdfd8035a2..197225f9c4 100644 --- a/apple/walkieware/Resources/Web/engine.mjs +++ b/apple/walkieware/Resources/Web/engine.mjs @@ -99,6 +99,7 @@ function jumpVersion(id) { render(source||'export function paint({wipe}) {wipe("black");}');review(false);phase(''); } setInterval(()=>document.querySelectorAll('#version-feed time').forEach(t=>t.textContent=relativeTime(t.dateTime)),15000); +let postPieces=()=>{}; let nativeTimer=null,nativeLedger=null,nativeRevisions=[],nativeLast='',captionSource=null,caption=''; function nativeSnapshot(){ if(!window.__walkiewareNativeShell||nativeTimer)return; @@ -113,6 +114,7 @@ function nativeSnapshot(){ const serialized=JSON.stringify(snapshot);if(serialized===nativeLast&&!historyChanged)return;nativeLast=serialized; if(historyChanged)snapshot.versions=nativeRevisions; post({action:'snapshot',snapshot}); + if(historyChanged)postPieces(); },80); } window.walkiewareNativeCommand=command=>{ @@ -124,6 +126,7 @@ window.walkiewareNativeCommand=command=>{ } if(command.action==='endPresentation'){presentedVersion=null;narrationPending=null;render(source);nativeSnapshot();} if(command.action==='newPiece'&&!busy)window.walkiewareNewPiece?.(); + if(command.action==='openPiece'&&!busy&&typeof command.piece==='string')window.walkiewareOpenPiece?.(command.piece); if(command.action==='retry'&&!busy)void resumeAttempt(true); if(command.action==='stop')$('live-stop').click(); if(command.action==='signIn')post({action:'signIn'}); @@ -366,6 +369,14 @@ if(versions&&['working','failed','interrupted'].includes(lastAttempt?.status)){ localStorage.removeItem(storageKey+'-inflight');lastAttempt={...lastAttempt,status:'completed',error:''}; }else if(lastAttempt.status==='working'){lastAttempt={...lastAttempt,status:'interrupted',error:'Interrupted before finishing'};} } +// Simulator fixture: hold the pending row open with streamed code so the busy +// state can be screenshot without a model or an account. Never leaves DEBUG. +if(typeof window.__walkiewareFixtureBusy==='string'){ + busy=true;ui.hidden=false;document.body.classList.add('live-mode','live-preview'); + lastAttempt={request:window.__walkiewareFixtureBusy,parent:versions?.head.id??0,status:'working',startedAt:new Date().toISOString()}; + outputStream='export function paint({ wipe, ink, circle, screen }) {\n wipe("#151838");\n ink("#4653c6");\n circle(screen.width / 2, screen.height / 2, 40, true);'; + $('live-code').textContent=outputStream;phase('Writing…');$('live-stop').hidden=false;review(false); +} updateFeed(); if(versions&&!window.__walkiewareSequence&&!window.__walkiewareBenchmark&&!window.__walkiewareDisableThread) { const label=codeLabel;label.setAttribute('aria-live','polite'); @@ -399,12 +410,44 @@ if(versions&&!window.__walkiewareSequence&&!window.__walkiewareBenchmark&&!windo const newPiece=document.createElement('button');newPiece.textContent='New piece';newPiece.style.cssText='font:24px Comic,Arial;padding:14px'; newPiece.onclick=window.walkiewareNewPiece=()=>{ if(busy)return; - const archive='walkieware-archive-'+thread.identity.id; - localStorage.setItem(archive,JSON.stringify({identity:thread.identity,ledger:versions.value,source})); + archiveCurrentPiece(); + location.reload(); + }; + // Every piece on this phone: the open one plus the archives "New piece" left. + // Opening another swaps archives, so the current one is never lost. + const ARCHIVE='walkieware-archive-',ARCHIVE_SUFFIXES=['-cloud-revision','-cloud-ledger']; + function archiveCurrentPiece(){ + const extras={};for(const suffix of ARCHIVE_SUFFIXES){const v=localStorage.getItem(storageKey+suffix);if(v!==null)extras[suffix]=v;} + localStorage.setItem(ARCHIVE+thread.identity.id,JSON.stringify({identity:thread.identity,ledger:versions.value,source,extras,archivedAt:new Date().toISOString()})); thread.suspend(); for(const suffix of ['', '-versions','-thread','-cloud-revision','-cloud-ledger','-attempt','-inflight'])localStorage.removeItem(storageKey+suffix); + } + function pieceSummary(id,identity,ledger,current){ + const made=(ledger?.versions||[]).filter(v=>v.id>0),last=made.at(-1); + return {id,code:identity?.code||'',utterance:last?utterance(last.request).slice(0,160):'',versions:made.length,updatedAt:last?.createdAt||made[0]?.createdAt||'',current}; + } + function pieceList(){ + const list=[pieceSummary(thread.identity.id,thread.identity,versions.value,true)]; + for(let i=0;i(b.current-a.current)||(Date.parse(b.updatedAt)||0)-(Date.parse(a.updatedAt)||0)).slice(0,256); + } + postPieces=()=>post({action:'pieces',pieces:pieceList()}); + window.walkiewareOpenPiece=id=>{ + if(busy||id===thread.identity.id)return; + let saved;try{saved=JSON.parse(localStorage.getItem(ARCHIVE+id));}catch{} + if(!saved?.identity?.id||!saved.ledger){phase('That piece is no longer on this phone');postPieces();return;} + archiveCurrentPiece(); + localStorage.setItem(storageKey,saved.source||''); + localStorage.setItem(storageKey+'-versions',JSON.stringify(saved.ledger)); + localStorage.setItem(storageKey+'-thread',JSON.stringify(saved.identity)); + for(const [suffix,value] of Object.entries(saved.extras||{}))localStorage.setItem(storageKey+suffix,value); + localStorage.removeItem(ARCHIVE+id); location.reload(); }; + postPieces(); $('info').append(newPiece); } post({action:'account'}); diff --git a/apple/walkieware/Sources/NativeScreenFixture.swift b/apple/walkieware/Sources/NativeScreenFixture.swift index 4857c4c194..ad9d06c923 100644 --- a/apple/walkieware/Sources/NativeScreenFixture.swift +++ b/apple/walkieware/Sources/NativeScreenFixture.swift @@ -6,7 +6,7 @@ enum NativeScreenFixture { static var mode: String { ProcessInfo.processInfo.environment["WALKIE_NATIVE_SCREEN_FIXTURE"] ?? "" } static var enabled: Bool { #if targetEnvironment(simulator) - return ["history", "recording", "gestures"].contains(mode) + return ["history", "recording", "gestures", "working"].contains(mode) #else return false #endif @@ -25,7 +25,8 @@ enum NativeScreenFixture { ] let ledger: [String: Any] = ["format": 1, "head": 3, "versions": versions] let json = String(data: try! JSONSerialization.data(withJSONObject: ledger), encoding: .utf8)! - return "window.__walkiewareDisableThread=true;localStorage.setItem('walkieware-source-versions',JSON.stringify(\(json)));localStorage.setItem('walkieware-source',\(json).versions[3].source);" + let busy = mode == "working" ? "window.__walkiewareFixtureBusy='Someone is eating them';" : "" + return busy + "window.__walkiewareDisableThread=true;localStorage.setItem('walkieware-source-versions',JSON.stringify(\(json)));localStorage.setItem('walkieware-source',\(json).versions[3].source);" } } #endif diff --git a/apple/walkieware/Sources/WalkiewareAccount.swift b/apple/walkieware/Sources/WalkiewareAccount.swift index 424aa513b0..4c524987ec 100644 --- a/apple/walkieware/Sources/WalkiewareAccount.swift +++ b/apple/walkieware/Sources/WalkiewareAccount.swift @@ -25,6 +25,10 @@ import UIKit } return tokens.accessToken } + /// Forgets the stored sign-in. The thread history stays on the device. + func signOut() { + SecItemDelete(key as CFDictionary) + } private func save(_ tokens: NativeSignIn.Tokens) throws { let data = try JSONEncoder().encode(tokens) let update = [kSecValueData as String: data] diff --git a/apple/walkieware/Sources/WalkiewareApp.swift b/apple/walkieware/Sources/WalkiewareApp.swift index 5c810b55a9..4bb62fc180 100644 --- a/apple/walkieware/Sources/WalkiewareApp.swift +++ b/apple/walkieware/Sources/WalkiewareApp.swift @@ -173,15 +173,18 @@ final class WalkiewareSession: NSObject, ObservableObject, WKScriptMessageHandle @Published var narratedFrame = 0 var narratedVersion: Int? @Published var workspaceReady = false + @Published var pieces: [PieceSummary] = [] - func command(_ action: String, version: Int? = nil, text: String? = nil) { - guard ["checkout", "newPiece", "stop", "signIn", "ask", "retry", "presentVersion", "endPresentation"].contains(action) else { return } - if action == "newPiece" { + func command(_ action: String, version: Int? = nil, text: String? = nil, piece: String? = nil) { + guard ["checkout", "newPiece", "openPiece", "stop", "signIn", "ask", "retry", "presentVersion", "endPresentation"].contains(action) else { return } + if action == "newPiece" || action == "openPiece" { guard engineReady, !snapshot.busy, capturePhase == .idle else { return } + if action == "openPiece" { guard let piece, pieces.contains(where: { $0.id == piece && !$0.current }) else { return } } previewSource = ""; engineReady = false } var value: [String: Any] = ["action": action] if let version { value["version"] = version } + if action == "openPiece", let piece { value["piece"] = piece } if action == "ask" { guard engineReady, !snapshot.busy, capturePhase == .idle, let text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, text.count <= 96 else { return } value["text"] = text @@ -194,6 +197,12 @@ final class WalkiewareSession: NSObject, ObservableObject, WKScriptMessageHandle webView?.evaluateJavaScript("voiceStart()") } func endHold() { webView?.evaluateJavaScript("voiceEnd()") } + /// Drops the Keychain sign-in and tells the engine, which parks its sockets. + func signOut() { + guard capturePhase == .idle else { return } + account.signOut() + emitEngine(["kind": "account", "token": ""]) + } func cancelHold() { webView?.evaluateJavaScript("voiceEnd(true)"); cancel() } @Published var startupFailure: String? @@ -304,11 +313,23 @@ final class WalkiewareSession: NSObject, ObservableObject, WKScriptMessageHandle next.handle = "preview"; next.code = "wwDemo" next.colors = [[220,180,255],[150,150,255],[255,170,80],[90,220,140],[90,215,210],[200,140,255],[255,140,210],[150,225,90]] if !engineReady && NativeScreenFixture.mode == "recording" { capturePhase = .recording; captureStarted = Date(); transcript = "Make the moon bounce like this…" } + // The fixture runs without a cloud thread, so the engine posts no + // piece list; seed one so the pieces sheet can be exercised. + if pieces.isEmpty { + let formatter = ISO8601DateFormatter() + pieces = [PieceSummary(id: "fixture", code: "wwDemo", utterance: "Add a little moon", versions: 3, updatedAt: formatter.string(from: Date().addingTimeInterval(-30)), current: true), + PieceSummary(id: "fixture-pond", code: "wwPond", utterance: "A frog on a lily pad", versions: 5, updatedAt: formatter.string(from: Date().addingTimeInterval(-5400)), current: false)] + } } #endif snapshot = next; engineReady = true case "voiceIdle": capturePhase = .idle; captureStarted = nil + case "pieces": + guard let value = body["pieces"] as? [[String: Any]], value.count <= 256, + let data = try? JSONSerialization.data(withJSONObject: value), + let next = try? JSONDecoder().decode([PieceSummary].self, from: data) else { return } + pieces = next case "threadStatus": if let code = body["code"] as? String, let status = body["status"] as? String { @@ -499,9 +520,10 @@ final class WalkiewareSession: NSObject, ObservableObject, WKScriptMessageHandle held = false AudioBenchmark.mark("releaseReceived") guard request != nil else { cancel(); return } - // Capture a short release tail, within the eight-second recording budget. + // Capture a release tail, within the eight-second recording budget. A + // thumb lifts a beat before the last word lands; 0.25 s clipped endings. let elapsed = captureStarted.map { Date().timeIntervalSince($0) } ?? 8 - let tail = min(0.25, max(0, 8 - elapsed)) + let tail = min(0.45, max(0, 8 - elapsed)) emit("processing") finishing?.cancel() let id = turn @@ -513,13 +535,12 @@ final class WalkiewareSession: NSObject, ObservableObject, WKScriptMessageHandle AudioBenchmark.mark("audioDrained") if self.recognitionFinal { self.deliver(); return } // endAudio lets recognition consume every queued buffer and finalize. + // If it never does, the partial transcript is still the person's + // words: send what was heard rather than throw the utterance away. try? await Task.sleep(for: .seconds(3)) guard !Task.isCancelled, self.turn == id, !self.delivering else { return } - if self.latest.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - self.deliver() // Preserve nonverbal musical input. - } else { - self.fail("Speech didn’t finish transcribing. Please try again.") - } + AudioBenchmark.mark("recognitionTimedOut", fields: ["recognizedCharacters": self.latest.count]) + self.deliver() } } diff --git a/apple/walkieware/Sources/WalkiewareHeader.swift b/apple/walkieware/Sources/WalkiewareHeader.swift new file mode 100644 index 0000000000..626278d841 --- /dev/null +++ b/apple/walkieware/Sources/WalkiewareHeader.swift @@ -0,0 +1,153 @@ +import SwiftUI + +// What the engine knows about every piece on this phone: the one that is open +// and the ones archived by "New piece". The engine posts the list on load and +// whenever it changes; opening one swaps the archives and reloads. +struct PieceSummary: Decodable, Identifiable { + let id: String + let code: String + let utterance: String + let versions: Int + let updatedAt: String + let current: Bool + var date: Date? { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.date(from: updatedAt) ?? ISO8601DateFormatter().date(from: updatedAt) + } + var title: String { code.isEmpty ? "untitled" : code } +} + +// Tapping the piece name: every piece, newest first, and a way to start one. +struct PiecesSheet: View { + let pieces: [PieceSummary] + let colors: [[Double]] + let disabled: Bool + let open: (String) -> Void + let newPiece: () -> Void + @Environment(\.dismiss) private var dismiss + var body: some View { + NavigationStack { + List { + Section { + Button { newPiece(); dismiss() } label: { + Label("New piece", systemImage: "plus") + .font(.custom("ComicRelief-Bold", size: 20, relativeTo: .title3)) + }.disabled(disabled).accessibilityIdentifier("pieces-new") + } + Section(pieces.count == 1 ? "Your piece" : "Your pieces") { + ForEach(pieces) { piece in + Button { + if !piece.current { open(piece.id) } + dismiss() + } label: { + HStack(alignment: .firstTextBaseline, spacing: 12) { + ComicTitle(text: piece.title, size: 22) + VStack(alignment: .trailing, spacing: 2) { + Text(piece.utterance.isEmpty ? "No requests yet" : piece.utterance) + .lineLimit(1).truncationMode(.tail) + .font(.custom("ComicRelief-Regular", size: 17, relativeTo: .body)) + Text(detail(piece)).font(.footnote).foregroundStyle(.secondary) + }.frame(maxWidth: .infinity, alignment: .trailing) + if piece.current { Image(systemName: "checkmark").foregroundStyle(.secondary).accessibilityLabel("Open now") } + } + } + .disabled(disabled && !piece.current) + .accessibilityIdentifier("piece-" + piece.title) + } + } + } + .navigationTitle("Pieces") + .navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } + } + .presentationDetents([.medium, .large]) + } + private func detail(_ piece: PieceSummary) -> String { + let versions = piece.versions == 1 ? "1 version" : "\(piece.versions) versions" + guard let date = piece.date else { return versions } + return versions + " · " + date.formatted(.relative(presentation: .named)) + } +} + +// Tapping the handle: who is signed in, and the door out. Settings grow here. +struct AccountSheet: View { + let handle: String + let colors: [[Double]] + @Binding var appearance: String + let signIn: () -> Void + let signOut: () -> Void + @Environment(\.dismiss) private var dismiss + @State private var confirmingSignOut = false + var body: some View { + NavigationStack { + List { + Section { + if handle.isEmpty { + Button { signIn(); dismiss() } label: { Label("Sign in to Aesthetic Computer", systemImage: "person") } + } else { + HStack { + ComicTitle(text: "@" + handle, colors: colors, size: 26) + Spacer() + Text("signed in").font(.footnote).foregroundStyle(.secondary) + } + } + } + Section("Appearance") { + Picker("Appearance", selection: $appearance) { + Text("System").tag("system") + Text("Light").tag("light") + Text("Dark").tag("dark") + }.pickerStyle(.segmented) + } + if !handle.isEmpty { + Section { + Button(role: .destructive) { confirmingSignOut = true } label: { Label("Sign out", systemImage: "rectangle.portrait.and.arrow.right") } + .accessibilityIdentifier("account-sign-out") + } footer: { Text("Your pieces stay on this phone. Making new versions needs a signed-in handle.") } + } + } + .navigationTitle(handle.isEmpty ? "Account" : "@" + handle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } + .confirmationDialog("Sign out of @" + handle + "?", isPresented: $confirmingSignOut, titleVisibility: .visible) { + Button("Sign out", role: .destructive) { signOut(); dismiss() } + } + } + .presentationDetents([.medium]) + } +} + +// The top-left identity, split in two: the handle opens the account sheet and +// the /code opens the pieces sheet. +struct IdentityHeader: View { + @ObservedObject var session: WalkiewareSession + @Binding var appearance: String + let size: CGFloat + let beforeOpening: () -> Void + @State private var showingPieces = false + @State private var showingAccount = false + private var handleText: String { session.snapshot.handle.isEmpty ? "Sign in" : "@" + session.snapshot.handle } + var body: some View { + HStack(spacing: 0) { + Button { beforeOpening(); showingAccount = true } label: { + ComicTitle(text: handleText, colors: session.snapshot.colors, size: size) + }.buttonStyle(.plain).accessibilityLabel(handleText + ", account").accessibilityIdentifier("workspace-account") + if !session.snapshot.code.isEmpty { + Button { beforeOpening(); showingPieces = true } label: { + ComicTitle(text: "/" + session.snapshot.code, size: size) + }.buttonStyle(.plain).accessibilityLabel(session.snapshot.code + ", pieces").accessibilityIdentifier("workspace-settings") + } + } + .sheet(isPresented: $showingPieces) { + PiecesSheet(pieces: session.pieces, colors: session.snapshot.colors, + disabled: session.snapshot.busy || session.capturePhase != .idle, + open: { session.command("openPiece", piece: $0) }, + newPiece: { session.command("newPiece") }) + } + .sheet(isPresented: $showingAccount) { + AccountSheet(handle: session.snapshot.handle, colors: session.snapshot.colors, appearance: $appearance, + signIn: { session.command("signIn") }, signOut: { session.signOut() }) + } + } +} diff --git a/apple/walkieware/Sources/WalkiewareScreen.swift b/apple/walkieware/Sources/WalkiewareScreen.swift index 7dc81b6e9f..5b1652ed78 100644 --- a/apple/walkieware/Sources/WalkiewareScreen.swift +++ b/apple/walkieware/Sources/WalkiewareScreen.swift @@ -80,17 +80,7 @@ struct WalkiewareScreen: View { var body: some View { VStack(spacing: session.layout.spacing) { HStack { - Menu { - Button("New piece", systemImage: "plus") { narrator.stop(); showComposer = false; session.command("newPiece") }.disabled(session.snapshot.busy || session.capturePhase != .idle) - Picker("Appearance", selection: $appearance) { - Text("System").tag("system") - Text("Light").tag("light") - Text("Dark").tag("dark") - } - Button(session.snapshot.handle.isEmpty ? "Sign in" : "@" + session.snapshot.handle, systemImage: "person") { session.command("signIn") } - } label: { - ComicTitle(text: identity, colors: session.snapshot.colors, size: session.layout.titleSize) - }.accessibilityLabel(identity + ", settings").accessibilityIdentifier("workspace-settings") + IdentityHeader(session: session, appearance: $appearance, size: session.layout.historySize) { narrator.stop(); showComposer = false } Spacer() Button { showComposer = false diff --git a/apple/walkieware/Tests/checks.json b/apple/walkieware/Tests/checks.json index 6e3365bc07..52322d876b 100644 --- a/apple/walkieware/Tests/checks.json +++ b/apple/walkieware/Tests/checks.json @@ -779,5 +779,71 @@ "AcServer tests passed" ], "limits": "Needs a lith deploy before the phone benefits; the deployed relay drops the field. Live re-measure owed after deploy." + }, + "selectorAndTicker": { + "build": 74, + "installedAndLaunched": true, + "features": [ + "One-line code ticker in pending status row", + "Fixed top selector with animated taps and progressive row fading", + "Hidden internal v0 in history and playback", + "Header uses same size as version labels", + "Fullscreen preview border and full utterance caption with synchronized word emphasis" + ], + "validation": [ + "Device build passed", + "Native scroll and fullscreen playback tests both passed (22.4 seconds total)", + "Timestamp and repeated-word highlight checks passed", + "Normal selector screenshot visually inspected" + ], + "limits": [ + "Tap animation timing not measured in native test", + "Code ticker manually inspected in source; no generation screenshot this turn" + ] + }, + "narrationAndFrog": { + "build": 76, + "installed": true, + "validation": [ + "Caption browser test passed with mocked speech callbacks", + "Disk worker bundle rebuilt", + "Device build passed", + "wwGikur original code: 1760 non-finite drawing calls over 160 frames; repaired: zero", + "Guarded wwGikur repair accepted as v2, no runtime errors reported" + ], + "limits": [ + "Web speech voices and word boundary availability depend on browser/device", + "Cloud captions have utterance text, not word alignment", + "No physical-device audible narration check", + "wwTovoz monitored through v10; existing narrator request is still implemented as text, not speech in its saved source" + ] + }, + "headerSheets": { + "build": 77, + "installed": true, + "behavior": "Top-left identity split: tapping @handle opens an Account sheet (handle, appearance picker, Sign out with confirmation; Sign in when signed out); tapping /code opens a Pieces sheet listing every piece on the phone (code, last utterance, version count, relative time) with New piece. Opening another piece archives the current one (ledger, source, thread identity, cloud sync state) and restores the chosen archive, then reloads.", + "validation": [ + "Simulator build passed", + "native-bridge browser test passed (32 local edits, recovery, layout tokens)", + "Device build passed; installed on the iPhone 14 Pro Max (launch blocked: phone locked)", + "HeaderSheetsTests UI test: see headerSheetsUITest" + ], + "limits": "Sign out keeps pieces on the phone; a signed-out device cannot make versions. Archived pieces live in localStorage only." + }, + "busyFixture": { + "build": 77, + "mode": "WALKIE_NATIVE_SCREEN_FIXTURE=working", + "behavior": "Simulator-only fixture holds the pending row open with streamed code and a working attempt so the busy UI (spinner, code ticker, kidlisp stop disc) can be screenshot without a model or account.", + "validation": [ + "Screenshot reviewed: pending row with stop disc above v3/v2/v1 in the working theme" + ] + }, + "releaseTail": { + "build": 77, + "change": "Release tail 0.25 s \u2192 0.45 s within the 8 s budget; when recognition does not finalize in 3 s the partial transcript is delivered instead of failing with \"Speech didn\u2019t finish transcribing\".", + "validation": [ + "Device build passed" + ], + "limits": "Not yet auditioned by voice on the phone this turn." } } \ No newline at end of file diff --git a/apple/walkieware/UITests/HeaderSheetsTests.swift b/apple/walkieware/UITests/HeaderSheetsTests.swift new file mode 100644 index 0000000000..a38c0a2d61 --- /dev/null +++ b/apple/walkieware/UITests/HeaderSheetsTests.swift @@ -0,0 +1,35 @@ +import XCTest + +// The top-left identity: the handle opens the account sheet, the /code opens +// the pieces sheet with a way to start a new piece. Runs on the simulator +// against the "history" fixture, which signs nothing in and reaches no cloud. +final class HeaderSheetsTests: XCTestCase { + private func launch() -> XCUIApplication { + let app = XCUIApplication() + app.launchEnvironment["WALKIE_NATIVE_SCREEN_FIXTURE"] = "history" + app.launch() + return app + } + + func testPieceCodeOpensPiecesSheetWithNewPiece() { + let app = launch() + let code = app.buttons["workspace-settings"] + XCTAssertTrue(code.waitForExistence(timeout: 20), "the /code title should be tappable") + code.tap() + XCTAssertTrue(app.buttons["pieces-new"].waitForExistence(timeout: 10), "the pieces sheet offers New piece") + XCTAssertTrue(app.buttons["piece-wwDemo"].exists, "the open piece is listed by its code") + app.buttons["Done"].tap() + XCTAssertFalse(app.buttons["pieces-new"].waitForExistence(timeout: 2)) + } + + func testHandleOpensAccountSheetWithSignOut() { + let app = launch() + let handle = app.buttons["workspace-account"] + XCTAssertTrue(handle.waitForExistence(timeout: 20), "the handle should be tappable") + handle.tap() + XCTAssertTrue(app.buttons["account-sign-out"].waitForExistence(timeout: 10), "a signed-in fixture handle shows Sign out") + XCTAssertTrue(app.segmentedControls.firstMatch.exists, "appearance moved into the account sheet") + app.buttons["Done"].tap() + XCTAssertFalse(app.buttons["account-sign-out"].waitForExistence(timeout: 2)) + } +} diff --git a/apple/walkieware/Walkieware.xcodeproj/project.pbxproj b/apple/walkieware/Walkieware.xcodeproj/project.pbxproj index b8e3c0921d..04d1bd97a4 100644 --- a/apple/walkieware/Walkieware.xcodeproj/project.pbxproj +++ b/apple/walkieware/Walkieware.xcodeproj/project.pbxproj @@ -15,10 +15,14 @@ 386E3D38C79AB6CF23166708 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 5E4DE7A1D969E42D910F4BCB /* Fixtures */; }; 3BF2B70E0C018C0D43C0134A /* WalkiewareScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220B6400DBD8900B5058A7F0 /* WalkiewareScreen.swift */; }; 3D181C343F0CF5296E1C4035 /* SwipeToTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA4916078A47F08CDF99B5E /* SwipeToTypeTests.swift */; }; + 465DCE826B91CD18CE48CEF7 /* PlaybackCaption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C614C7FAFB7C430BF574D1B9 /* PlaybackCaption.swift */; }; 4EDE3317EA5C40F16CB7E32A /* WalkiewareTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB854B6224E9C6EC2FC901D /* WalkiewareTheme.swift */; }; + 5FD9722A7E3D5329D7B1E770 /* CodeTicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654A6A5F8172661179904170 /* CodeTicker.swift */; }; + 6380DEC1F53B213D184FD656 /* WalkiewareHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D832B5A1A03E760A43BBF8 /* WalkiewareHeader.swift */; }; 6D7B5EBEDBC462D5029C784D /* Web in Resources */ = {isa = PBXBuildFile; fileRef = 315FD6C024E525106C7FD3BE /* Web */; }; 7EC06C94440CDAF9B846F0BB /* SequenceBenchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F16D0B24CA61442FFD0E27 /* SequenceBenchmark.swift */; }; 8681CA8AC5E530EAD0589219 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 110077D3E4AF92775C4E6D74 /* Assets.xcassets */; }; + 8CD6B532DB3A5B03BC35EEDD /* HeaderSheetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B2C6F3854EDC996115B080 /* HeaderSheetsTests.swift */; }; 91EBE2405BCB456959613E70 /* RecordingTrim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35720E8232DBFB5B795FBEEA /* RecordingTrim.swift */; }; 97AB97086118FC695F4B2133 /* AudioBenchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F6D30D7883EC26BF391CC7 /* AudioBenchmark.swift */; }; 99B11F258C6DD8DEAD32A419 /* MusicalInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7C5768F0EE9F4A8A288767 /* MusicalInput.swift */; }; @@ -40,6 +44,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 03D832B5A1A03E760A43BBF8 /* WalkiewareHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalkiewareHeader.swift; sourceTree = ""; }; 06F16D0B24CA61442FFD0E27 /* SequenceBenchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceBenchmark.swift; sourceTree = ""; }; 110077D3E4AF92775C4E6D74 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1A7C5768F0EE9F4A8A288767 /* MusicalInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicalInput.swift; sourceTree = ""; }; @@ -52,10 +57,13 @@ 4A3D6D0D1C0C32051B6C6AF4 /* UtteranceRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtteranceRecording.swift; sourceTree = ""; }; 50D5B994181BB8DD173DBA13 /* PlaybackWord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackWord.swift; sourceTree = ""; }; 5E4DE7A1D969E42D910F4BCB /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Fixtures; path = Resources/Fixtures; sourceTree = SOURCE_ROOT; }; + 654A6A5F8172661179904170 /* CodeTicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeTicker.swift; sourceTree = ""; }; + 67B2C6F3854EDC996115B080 /* HeaderSheetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSheetsTests.swift; sourceTree = ""; }; 690D4836B59F77E01D9326D4 /* NetworkBenchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkBenchmark.swift; sourceTree = ""; }; 706045AC87F69577059E617E /* Walkieware.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Walkieware.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7BB854B6224E9C6EC2FC901D /* WalkiewareTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalkiewareTheme.swift; sourceTree = ""; }; 92FF8B832524B3AC4D8291F7 /* WalkiewareUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = WalkiewareUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C614C7FAFB7C430BF574D1B9 /* PlaybackCaption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCaption.swift; sourceTree = ""; }; CBA7AE20BF7B4A303BD2F09A /* WalkiewareApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalkiewareApp.swift; sourceTree = ""; }; D40BA9A3C78CC0324903812B /* WorkspaceCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceCoordinator.swift; sourceTree = ""; }; E8E12122840B2375C0AB2ED0 /* VersionNarrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNarrator.swift; sourceTree = ""; }; @@ -78,10 +86,12 @@ isa = PBXGroup; children = ( 45F6D30D7883EC26BF391CC7 /* AudioBenchmark.swift */, + 654A6A5F8172661179904170 /* CodeTicker.swift */, 49B317E893F3D8506A3F246D /* InputButtonLabels.swift */, 1A7C5768F0EE9F4A8A288767 /* MusicalInput.swift */, FF403AD7661862BCAC9B2F95 /* NativeScreenFixture.swift */, 690D4836B59F77E01D9326D4 /* NetworkBenchmark.swift */, + C614C7FAFB7C430BF574D1B9 /* PlaybackCaption.swift */, 50D5B994181BB8DD173DBA13 /* PlaybackWord.swift */, 35720E8232DBFB5B795FBEEA /* RecordingTrim.swift */, 06F16D0B24CA61442FFD0E27 /* SequenceBenchmark.swift */, @@ -89,6 +99,7 @@ E8E12122840B2375C0AB2ED0 /* VersionNarrator.swift */, F3EC37299741D5E90E45EE5F /* WalkiewareAccount.swift */, CBA7AE20BF7B4A303BD2F09A /* WalkiewareApp.swift */, + 03D832B5A1A03E760A43BBF8 /* WalkiewareHeader.swift */, 220B6400DBD8900B5058A7F0 /* WalkiewareScreen.swift */, 7BB854B6224E9C6EC2FC901D /* WalkiewareTheme.swift */, D40BA9A3C78CC0324903812B /* WorkspaceCoordinator.swift */, @@ -99,6 +110,7 @@ 8FD6F530EA415417DBA563F3 /* UITests */ = { isa = PBXGroup; children = ( + 67B2C6F3854EDC996115B080 /* HeaderSheetsTests.swift */, 3EA4916078A47F08CDF99B5E /* SwipeToTypeTests.swift */, ); path = UITests; @@ -231,6 +243,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8CD6B532DB3A5B03BC35EEDD /* HeaderSheetsTests.swift in Sources */, 3D181C343F0CF5296E1C4035 /* SwipeToTypeTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -240,11 +253,13 @@ buildActionMask = 2147483647; files = ( 97AB97086118FC695F4B2133 /* AudioBenchmark.swift in Sources */, + 5FD9722A7E3D5329D7B1E770 /* CodeTicker.swift in Sources */, 0DAF293EA0DCA59330C836C6 /* InputButtonLabels.swift in Sources */, 99B11F258C6DD8DEAD32A419 /* MusicalInput.swift in Sources */, C57CC34DE256A25EDF176C4A /* NativeScreenFixture.swift in Sources */, F9A4BC365780D687A6B8CADB /* NativeSignIn.swift in Sources */, 349E23AC69F23D16290DCB48 /* NetworkBenchmark.swift in Sources */, + 465DCE826B91CD18CE48CEF7 /* PlaybackCaption.swift in Sources */, 09A89EDEB03772B566D345A9 /* PlaybackWord.swift in Sources */, 91EBE2405BCB456959613E70 /* RecordingTrim.swift in Sources */, 7EC06C94440CDAF9B846F0BB /* SequenceBenchmark.swift in Sources */, @@ -252,6 +267,7 @@ 27410D22F96D8006B6D29452 /* VersionNarrator.swift in Sources */, 28DE33D8B94B853B408571A1 /* WalkiewareAccount.swift in Sources */, FC34834B44AA5AF9FDB73AC0 /* WalkiewareApp.swift in Sources */, + 6380DEC1F53B213D184FD656 /* WalkiewareHeader.swift in Sources */, 3BF2B70E0C018C0D43C0134A /* WalkiewareScreen.swift in Sources */, 4EDE3317EA5C40F16CB7E32A /* WalkiewareTheme.swift in Sources */, E0B6A4DB98A5C44344C74589 /* WorkspaceCoordinator.swift in Sources */, @@ -374,7 +390,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 77; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -441,7 +457,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 77; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_NS_ASSERTIONS = NO; diff --git a/apple/walkieware/project.yml b/apple/walkieware/project.yml index cd73c3ade4..3439ec3f7e 100644 --- a/apple/walkieware/project.yml +++ b/apple/walkieware/project.yml @@ -9,7 +9,7 @@ settings: CODE_SIGN_STYLE: Automatic SWIFT_VERSION: "5.0" MARKETING_VERSION: "0.1.0" - CURRENT_PROJECT_VERSION: "71" + CURRENT_PROJECT_VERSION: "77" targets: Walkieware: type: application