From f8e19706cc7d3aecd3ab438b298c87712bc72ac2 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 15:34:46 +0800 Subject: [PATCH 1/5] Stabilize iPad system UI actions --- CHANGELOG.en.md | 10 + CHANGELOG.md | 6 + .../experimental-v0.2.0/COMPOSITION.json | 2 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +- .../UITests/PocketRootHostAppUITests.swift | 177 +++++++++++++++--- Tests/Scripts/ReleaseComplianceTests.rb | 9 +- Tests/Scripts/RuntimeSmokeScriptTests.sh | 6 +- 8 files changed, 187 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index e206a2e..c0ba09f 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -28,6 +28,16 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit infrastructure failure, restarting only its own temporary Simulator for one bounded retry; caller-owned devices, ordinary assertions, and a second failure still fail closed immediately. +- Host App iPad file-creation smoke no longer semantically taps `New File` or + `New Folder` directly in a popover. It revalidates the App and action frames, + performs a bounded physical-coordinate retry, and attaches the accessibility + hierarchy plus final frames if the name field never appears. Entering the + local document-picker location now also re-queries the sidebar item before + each attempt, uses a captured coordinate first and an independent semantic + action path second, and waits for either Host navigation state or the Host + container. If neither path completes the transition, hierarchy evidence is + retained instead of repeating an event against one stale accessibility + snapshot. - Tightened the untagged `v0.2.0` source-candidate audit: `--allow-source-blocked` accepts only final source-release authorization as unsatisfied, and fails on any pinned-gate-set, NOTICE, license, public API diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b0c30..4d67267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 正在消失的元素。通用 UI runner 也会把 XCTest 在测试方法执行前报告的 Accessibility 加载超时识别为模拟器基础设施故障,仅对自己创建的临时 Simulator 重启并有界重试一次;调用方提供的设备、普通测试断言和第二次失败仍立即 fail-closed。 +- Host App iPad 文件创建 smoke 不再直接语义点击 popover 中的 `New File` / `New Folder`: + 测试会重新校验 App 与动作 frame、通过物理坐标有界重试,并在名称输入框未出现时附带 + accessibility hierarchy 与最后 frame 立即失败。系统文件选择器进入本机位置时也会在 + 每次尝试前重新查询 sidebar 条目,首次使用已捕获坐标、第二次使用独立的语义动作路径, + 同时等待 Host 导航状态或容器条目;两条路径都未完成转换时保留层级证据,不再重复操作 + 同一个过期 accessibility snapshot。 - 收紧 `v0.2.0` 未打 tag 源码候选审计:`--allow-source-blocked` 只接受最终源码发布 授权这一项尚未满足,固定门禁集合、NOTICE、许可证、公开 API 状态或源码边界漂移 都会失败;CI 和受信 tag 工作流只上传带 commit、archive SHA-256、文件统计和精确 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index b5e7b59..04d138e 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "d925256c6207a0ef676c260fc023a41d86df5e5c8d494957b0102c2266a3a228", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "9e3e0916d18ca901e821d0ca41a7d5935bbd57b0fdb30e3deb368994b47cc259", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "9d1c2a16adbd6918ea86d36808bfe360047ecb222d38afca2efd8db2d2eaf373", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index bf3867e..89c668e 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/a9a4d9f55109789e23abb37c2f0c609173374996e45e8ce7e54c74e621d61b35", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/143e671eccb9ab22ed4f9fa095cffaeeb7a2063b2d9bdb0e5120b03f9e0129b4", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index c54c06f..68d0c6a 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -a9a4d9f55109789e23abb37c2f0c609173374996e45e8ce7e54c74e621d61b35 COMPOSITION.json +143e671eccb9ab22ed4f9fa095cffaeeb7a2063b2d9bdb0e5120b03f9e0129b4 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -e1673e7cba02ebe7dc563b01906517d7cd676803b68b199e63bf083ee1b9c0e9 SBOM.spdx.json +41fcf92309302c3f9793e7220c959a3be3a4ff4b050c143cfb7fef4a4f59fde0 SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index e56f316..8b598a9 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -40,9 +40,14 @@ final class PocketRootHostAppUITests: XCTestCase { actions.tap() XCTAssertTrue(app.buttons["Import File"].waitForExistence(timeout: 10)) - app.buttons["New File"].tap() let nameField = app.textFields["Name"] - XCTAssertTrue(nameField.waitForExistence(timeout: 10)) + guard openCreationDialog( + action: "New File", + nameField: nameField, + in: app + ) else { + return + } nameField.typeText(fileName) let file = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(fileName)" @@ -102,8 +107,13 @@ final class PocketRootHostAppUITests: XCTestCase { waitForEnabled(actions) actions.tap() - app.buttons["New Folder"].tap() - XCTAssertTrue(nameField.waitForExistence(timeout: 10)) + guard openCreationDialog( + action: "New Folder", + nameField: nameField, + in: app + ) else { + return + } nameField.typeText(folderName) let folder = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(folderName)" @@ -128,8 +138,13 @@ final class PocketRootHostAppUITests: XCTestCase { waitForEnabled(actions) actions.tap() - app.buttons["New File"].tap() - XCTAssertTrue(nameField.waitForExistence(timeout: 10)) + guard openCreationDialog( + action: "New File", + nameField: nameField, + in: app + ) else { + return + } nameField.typeText(nestedFileName) let nestedFile = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(folderName)/\(nestedFileName)" @@ -1028,7 +1043,7 @@ final class PocketRootHostAppUITests: XCTestCase { // file list. Spend one deadline waiting for either the fixture itself, // a local location, or a usable Browse transition. let pickerDeadline = Date().addingTimeInterval(60) - var localLocation: XCUIElement? + var foundLocalLocation = false var didTapBrowse = false while Date() < pickerDeadline { if currentHostDocuments.exists { @@ -1045,11 +1060,11 @@ final class PocketRootHostAppUITests: XCTestCase { continue } if sidebarLocalLocation.exists { - localLocation = sidebarLocalLocation + foundLocalLocation = true break } if fallbackLocalLocation.exists { - localLocation = fallbackLocalLocation + foundLocalLocation = true break } if !didTapBrowse, browse.exists { @@ -1077,7 +1092,7 @@ final class PocketRootHostAppUITests: XCTestCase { } return true } - guard let localLocation else { + guard foundLocalLocation else { XCTFail("document picker to expose a usable Host or local destination") return false } @@ -1098,7 +1113,9 @@ final class PocketRootHostAppUITests: XCTestCase { ) ).firstMatch var openedHostDestination = false - for _ in 0..<2 { + var lastLocalAppFrame = CGRect.null + var lastLocalElementFrame = CGRect.null + for attempt in 0..<2 { // Browse can restore the last Host destination while the local // location query is transitioning out of the accessibility tree. // Re-check that stronger navigation state before touching a stale @@ -1107,32 +1124,75 @@ final class PocketRootHostAppUITests: XCTestCase { openedHostDestination = true break } + + // Re-query on every attempt. iPad keeps its sidebar cell visible + // while changing the selected location, so retaining the element + // that originally won the readiness race can synthesize two taps + // against the same stale accessibility snapshot. + let currentLocalLocation: XCUIElement + if sidebarLocalLocation.exists { + currentLocalLocation = sidebarLocalLocation + } else if fallbackLocalLocation.exists { + currentLocalLocation = fallbackLocalLocation + } else { + if waitForDocumentPickerDestination( + currentHostDocuments: currentHostDocuments, + hostDestination: hostDestination, + timeout: 5 + ) { + openedHostDestination = true + break + } + continue + } guard let frames = waitForInteractionFrames( - of: localLocation, + of: currentLocalLocation, in: app, timeout: 5 ) else { - if currentHostDocuments.waitForExistence(timeout: 5) { + if waitForDocumentPickerDestination( + currentHostDocuments: currentHostDocuments, + hostDestination: hostDestination, + timeout: 5 + ) { openedHostDestination = true break } continue } - tapFrame( - frames.elementFrame, - in: frames.appFrame, - using: app - ) - if hostDestination.waitForExistence(timeout: 10) { + lastLocalAppFrame = frames.appFrame + lastLocalElementFrame = frames.elementFrame + if attempt == 0 { + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + } else { + // The semantic cell action uses a different XCTest event path + // than the captured coordinate. Keep it as the single bounded + // fallback only after validating the freshly queried frame. + currentLocalLocation.tap() + } + if waitForDocumentPickerDestination( + currentHostDocuments: currentHostDocuments, + hostDestination: hostDestination, + timeout: 10 + ) { openedHostDestination = true break } } - XCTAssertTrue( - openedHostDestination, - "local document location to reveal the host container" - ) guard openedHostDestination else { + attachHierarchy( + named: "Document picker failed to open local location", + from: app + ) + XCTFail( + "local document location to reveal the host container; " + + "app=\(lastLocalAppFrame), " + + "element=\(lastLocalElementFrame)" + ) return false } if !currentHostDocuments.exists { @@ -1315,6 +1375,77 @@ final class PocketRootHostAppUITests: XCTestCase { return nil } + private func openCreationDialog( + action actionName: String, + nameField: XCUIElement, + in app: XCUIApplication + ) -> Bool { + let action = app.buttons[actionName] + var lastAppFrame = CGRect.null + var lastActionFrame = CGRect.null + + // iPad can report a successful semantic menu-button tap while merely + // focusing the popover row. Use a validated physical coordinate, then + // re-query and retry that coordinate once only while the menu remains. + for _ in 0..<2 { + if nameField.exists { + return true + } + guard let frames = waitForInteractionFrames( + of: action, + in: app, + timeout: 5 + ) else { + break + } + lastAppFrame = frames.appFrame + lastActionFrame = frames.elementFrame + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + if nameField.waitForExistence(timeout: 10) { + return true + } + } + + attachHierarchy( + named: "\(actionName) did not open the creation dialog", + from: app + ) + XCTFail( + "\(actionName) creation dialog to appear; " + + "app=\(lastAppFrame), action=\(lastActionFrame)" + ) + return false + } + + private func waitForDocumentPickerDestination( + currentHostDocuments: XCUIElement, + hostDestination: XCUIElement, + timeout: TimeInterval + ) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if currentHostDocuments.exists || hostDestination.exists { + return true + } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } + return currentHostDocuments.exists || hostDestination.exists + } + + private func attachHierarchy( + named name: String, + from app: XCUIApplication + ) { + let attachment = XCTAttachment(string: app.debugDescription) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + private func openFileEntryContextMenu( for element: XCUIElement, expectedAction: String, diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index b2a6f59..32108ab 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1229,10 +1229,17 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke refute_includes ui_test, "fixture.tap()" refute_includes ui_test, "exported.tap()" assert_includes ui_test, "for _ in 0..<2" - assert_includes ui_test, "hostDestination.waitForExistence(timeout: 10)" + assert_includes ui_test, "waitForDocumentPickerDestination(" + assert_includes ui_test, "currentLocalLocation.tap()" + assert_includes ui_test, "lastLocalElementFrame" + assert_includes ui_test, "attachHierarchy(" assert_includes ui_test, "openedHostDestination" assert_includes ui_test, '"DOC.sidebar.item.On My iPad"' assert_includes ui_test, '"PocketRoot Host, Actions Menu"' + assert_equal 3, ui_test.scan("guard openCreationDialog(").length + assert_includes ui_test, "validated physical coordinate" + refute_includes ui_test, 'app.buttons["New File"].tap()' + refute_includes ui_test, 'app.buttons["New Folder"].tap()' refute_includes ui_test, "currentHostDocuments.waitForExistence(timeout: 3)" refute_includes( ui_test, diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index 3f9db16..a8f6eec 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -478,7 +478,11 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'then let the stronger Files entry and exact preview assertions below' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'UUID().uuidString.lowercased()' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'XCTAssertEqual(preview.label, integratedContents)' "$HOST_UI_TESTS" \ - || ! grep -Fq -- 'if currentHostDocuments.waitForExistence(timeout: 5)' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'openCreationDialog(' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'validated physical coordinate' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'waitForDocumentPickerDestination(' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'currentLocalLocation.tap()' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'attachHierarchy(' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'underlying host geometry' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'for attempt in 0..<2' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'relaunchAndBoot(app)' "$HOST_UI_TESTS" \ From e2c4f83afb561bad06941e9469f8964d27ac8d15 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 16:16:59 +0800 Subject: [PATCH 2/5] Avoid stale document picker cell actions --- CHANGELOG.en.md | 13 +++---- CHANGELOG.md | 6 ++-- .../experimental-v0.2.0/COMPOSITION.json | 2 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +-- .../UITests/PocketRootHostAppUITests.swift | 34 ++++++++++++------- Tests/Scripts/ReleaseComplianceTests.rb | 3 +- Tests/Scripts/RuntimeSmokeScriptTests.sh | 4 ++- 8 files changed, 40 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index c0ba09f..78958bf 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -32,12 +32,13 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit `New Folder` directly in a popover. It revalidates the App and action frames, performs a bounded physical-coordinate retry, and attaches the accessibility hierarchy plus final frames if the name field never appears. Entering the - local document-picker location now also re-queries the sidebar item before - each attempt, uses a captured coordinate first and an independent semantic - action path second, and waits for either Host navigation state or the Host - container. If neither path completes the transition, hierarchy evidence is - retained instead of repeating an event against one stale accessibility - snapshot. + local document-picker location now also re-queries its target before every + attempt, using the sidebar item first and a newly resolved text frame inside + that row second. Both attempts dispatch only through captured App + coordinates while waiting for either Host navigation state or the Host + container. If neither completes the transition, hierarchy evidence is kept + without asking a disappearing system cell to resolve and perform its own + action. - Tightened the untagged `v0.2.0` source-candidate audit: `--allow-source-blocked` accepts only final source-release authorization as unsatisfied, and fails on any pinned-gate-set, NOTICE, license, public API diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d67267..4ef9842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,9 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 - Host App iPad 文件创建 smoke 不再直接语义点击 popover 中的 `New File` / `New Folder`: 测试会重新校验 App 与动作 frame、通过物理坐标有界重试,并在名称输入框未出现时附带 accessibility hierarchy 与最后 frame 立即失败。系统文件选择器进入本机位置时也会在 - 每次尝试前重新查询 sidebar 条目,首次使用已捕获坐标、第二次使用独立的语义动作路径, - 同时等待 Host 导航状态或容器条目;两条路径都未完成转换时保留层级证据,不再重复操作 - 同一个过期 accessibility snapshot。 + 每次尝试前重新查询目标,首次使用 sidebar 条目、第二次使用条目内新解析的文字 frame, + 且两次都只通过已捕获的 App 坐标触发,同时等待 Host 导航状态或容器条目;两条路径都 + 未完成转换时保留层级证据,不会要求正在消失的系统 cell 重新解析并执行自身动作。 - 收紧 `v0.2.0` 未打 tag 源码候选审计:`--allow-source-blocked` 只接受最终源码发布 授权这一项尚未满足,固定门禁集合、NOTICE、许可证、公开 API 状态或源码边界漂移 都会失败;CI 和受信 tag 工作流只上传带 commit、archive SHA-256、文件统计和精确 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 04d138e..6a19074 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "9e3e0916d18ca901e821d0ca41a7d5935bbd57b0fdb30e3deb368994b47cc259", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "286619e55e735bb1db1cf004a45b047631502531fb3e9a2e1bfbaa7b5bbca448", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "9d1c2a16adbd6918ea86d36808bfe360047ecb222d38afca2efd8db2d2eaf373", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index 89c668e..c61555e 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/143e671eccb9ab22ed4f9fa095cffaeeb7a2063b2d9bdb0e5120b03f9e0129b4", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/74dcc8ce950d300f111e075bd1306c5f0180d50fa024edf4c3aa09d8514add38", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 68d0c6a..7016a49 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -143e671eccb9ab22ed4f9fa095cffaeeb7a2063b2d9bdb0e5120b03f9e0129b4 COMPOSITION.json +74dcc8ce950d300f111e075bd1306c5f0180d50fa024edf4c3aa09d8514add38 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -41fcf92309302c3f9793e7220c959a3be3a4ff4b050c143cfb7fef4a4f59fde0 SBOM.spdx.json +0228593a3079e780041c6610e135123a03a4af3094bce03b9f1b0dc2616d5d24 SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 8b598a9..c47c71c 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -1034,6 +1034,12 @@ final class PocketRootHostAppUITests: XCTestCase { localLocationLabels ) ).firstMatch + let localLocationLabel = app.staticTexts.matching( + NSPredicate( + format: "label IN %@", + localLocationLabels + ) + ).firstMatch let hostFixture = app.cells[ "\(Self.systemImportFixtureDisplayName), txt" ] @@ -1130,7 +1136,12 @@ final class PocketRootHostAppUITests: XCTestCase { // that originally won the readiness race can synthesize two taps // against the same stale accessibility snapshot. let currentLocalLocation: XCUIElement - if sidebarLocalLocation.exists { + if attempt == 1, localLocationLabel.exists { + // The label frame is an independent, freshly resolved target + // inside the same row. It avoids asking a cell that can vanish + // during the selection transition to execute its own action. + currentLocalLocation = localLocationLabel + } else if sidebarLocalLocation.exists { currentLocalLocation = sidebarLocalLocation } else if fallbackLocalLocation.exists { currentLocalLocation = fallbackLocalLocation @@ -1162,18 +1173,15 @@ final class PocketRootHostAppUITests: XCTestCase { } lastLocalAppFrame = frames.appFrame lastLocalElementFrame = frames.elementFrame - if attempt == 0 { - tapFrame( - frames.elementFrame, - in: frames.appFrame, - using: app - ) - } else { - // The semantic cell action uses a different XCTest event path - // than the captured coordinate. Keep it as the single bounded - // fallback only after validating the freshly queried frame. - currentLocalLocation.tap() - } + // Always synthesize through the application using captured values. + // Calling tap() on the query would resolve the element again, and + // the system picker can remove that cell between frame validation + // and event dispatch. + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) if waitForDocumentPickerDestination( currentHostDocuments: currentHostDocuments, hostDestination: hostDestination, diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 32108ab..ee0a0f1 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1230,7 +1230,8 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke refute_includes ui_test, "exported.tap()" assert_includes ui_test, "for _ in 0..<2" assert_includes ui_test, "waitForDocumentPickerDestination(" - assert_includes ui_test, "currentLocalLocation.tap()" + assert_includes ui_test, "localLocationLabel.exists" + refute_includes ui_test, "currentLocalLocation.tap()" assert_includes ui_test, "lastLocalElementFrame" assert_includes ui_test, "attachHierarchy(" assert_includes ui_test, "openedHostDestination" diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index a8f6eec..adec5a8 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -481,7 +481,9 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'openCreationDialog(' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'validated physical coordinate' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'waitForDocumentPickerDestination(' "$HOST_UI_TESTS" \ - || ! grep -Fq -- 'currentLocalLocation.tap()' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'localLocationLabel.exists' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'Always synthesize through the application using captured values' "$HOST_UI_TESTS" \ + || grep -Fq -- 'currentLocalLocation.tap()' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'attachHierarchy(' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'underlying host geometry' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'for attempt in 0..<2' "$HOST_UI_TESTS" \ From 722e10bb0c17f9ba64898a9627104a40c371164f Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 17:36:26 +0800 Subject: [PATCH 3/5] Present file exports correctly on iPad --- CHANGELOG.en.md | 10 ++ CHANGELOG.md | 6 + .../experimental-v0.2.0/COMPOSITION.json | 4 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +- .../UITests/PocketRootHostAppUITests.swift | 107 +++++++++++++++++- .../Public/PocketRootFileBrowserView.swift | 17 ++- Tests/Scripts/ReleaseComplianceTests.rb | 15 ++- Tests/Scripts/RuntimeSmokeScriptTests.sh | 5 + 9 files changed, 153 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 78958bf..d74b26a 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -39,6 +39,16 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit container. If neither completes the transition, hierarchy evidence is kept without asking a disappearing system cell to resolve and perform its own action. +- Files export now follows the UIKit presentation contract by showing + `UIActivityViewController` in a popover on iPad and adapting to a sheet only + in compact-width environments such as iPhone. This avoids the former iPad + modal path getting stuck with a dimmed background while the share content + remains below the screen. The temporary export directory is removed after + the presentation disappears whether the user completes, cancels, or taps + outside the share UI. A dedicated iPad export UI smoke now verifies the + platform activity UI and `Save to Files` action without depending on system + file import, and the system round-trip activates its share menu action + through a freshly validated physical frame. - Tightened the untagged `v0.2.0` source-candidate audit: `--allow-source-blocked` accepts only final source-release authorization as unsatisfied, and fails on any pinned-gate-set, NOTICE, license, public API diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef9842..4dd04ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 每次尝试前重新查询目标,首次使用 sidebar 条目、第二次使用条目内新解析的文字 frame, 且两次都只通过已捕获的 App 坐标触发,同时等待 Host 导航状态或容器条目;两条路径都 未完成转换时保留层级证据,不会要求正在消失的系统 cell 重新解析并执行自身动作。 +- Files 导出现在按 UIKit 平台契约在 iPad 使用 popover 呈现 + `UIActivityViewController`,并仅在 iPhone 等紧凑宽度环境适配为 sheet;这避免 iPad + 在先前的模态 sheet 路径中只显示变暗背景、分享内容仍停留在屏幕下方。无论用户完成、 + 取消还是点按外部关闭分享界面,临时导出目录都会随展示内容退场后清理。新增独立的 + iPad 导出 UI smoke,不依赖系统文件导入即可验证平台分享界面和 `Save to Files` 动作; + 系统 round-trip 的分享菜单动作也改为通过重新校验的物理 frame 触发。 - 收紧 `v0.2.0` 未打 tag 源码候选审计:`--allow-source-blocked` 只接受最终源码发布 授权这一项尚未满足,固定门禁集合、NOTICE、许可证、公开 API 状态或源码边界漂移 都会失败;CI 和受信 tag 工作流只上传带 commit、archive SHA-256、文件统计和精确 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 6a19074..9f3b719 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -91,7 +91,7 @@ "Sources/PocketRootTerminal/Bridge/TerminalBridge.swift": "b0850236d896a517258dc310c30f2f63dfaf4450cb7ff732a3ba3414c2da2002", "Sources/PocketRootTerminal/Public/PocketRootCommandTerminalSession.swift": "5eaa24f24efb46de1482029b3c5f2ce33942a9e961249fbdaeb728eabe92257a", "Sources/PocketRootTerminal/Public/PocketRootFileBrowser.swift": "2f74c5a311899995f8a941402ea5e3f7cad07a922fb341ff183e1b0795944801", - "Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift": "dfc40223b22bae241d210430a23886e8410d3c39114e8cc655a848d6e1e86318", + "Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift": "cc3a66da920eadba530cb40aed76469023426a9bf1d6e0d05e2a2b5c0d584473", "Sources/PocketRootTerminal/Public/PocketRootTerminalCommandExecutor.swift": "e9deacbb10e81adc412147a1a1b0a6f7896acfa2be4ef1b1a855e9cb01b09e61", "Sources/PocketRootTerminal/Public/PocketRootTerminalConfiguration.swift": "2207a7c358517ddf2d8791019b39ce4f6b27203b0763046e40f825384ab08d14", "Sources/PocketRootTerminal/Public/PocketRootTerminalTheme.swift": "a0f67e8d79b76465bb6b64d4ea3f48b886bc6a5d2d85a59669b82699e0e0649b", @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "286619e55e735bb1db1cf004a45b047631502531fb3e9a2e1bfbaa7b5bbca448", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "2a518e063f4a8ccaa4b97d8851821cd77f5eaf036a512c7af08bf242591e84c2", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "9d1c2a16adbd6918ea86d36808bfe360047ecb222d38afca2efd8db2d2eaf373", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index c61555e..caec26f 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/74dcc8ce950d300f111e075bd1306c5f0180d50fa024edf4c3aa09d8514add38", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/184e4a4f2472396b778a344215e90f7697cedaf1c6b9e0847e18ba3a6807db49", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 7016a49..1d221df 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -74dcc8ce950d300f111e075bd1306c5f0180d50fa024edf4c3aa09d8514add38 COMPOSITION.json +184e4a4f2472396b778a344215e90f7697cedaf1c6b9e0847e18ba3a6807db49 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -0228593a3079e780041c6610e135123a03a4af3094bce03b9f1b0dc2616d5d24 SBOM.spdx.json +6c1a8254582b5721fd4f0973a0bb3e54beb25fae9d63ad7824e3298ec85f04ed SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index c47c71c..5f5ed19 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -180,6 +180,85 @@ final class PocketRootHostAppUITests: XCTestCase { shutdownRuntime(in: app) } + func testFileExportUsesPlatformActivityPresentation() { + let app = launchAndBoot() + app.buttons["PocketRootHost.files"].tap() + + let suffix = String(UUID().uuidString.prefix(8)).lowercased() + let fileName = "share-\(suffix).txt" + let file = app.descendants(matching: .any)[ + "PocketRootFiles.entry./root/\(fileName)" + ] + let actions = app.buttons["PocketRootFiles.actions"] + XCTAssertTrue(actions.waitForExistence(timeout: 30)) + waitForEnabled(actions) + actions.tap() + let nameField = app.textFields["Name"] + guard openCreationDialog( + action: "New File", + nameField: nameField, + in: app + ) else { + return + } + nameField.typeText(fileName) + guard submitCreation(expectedEntry: file, in: app), + revealFileEntry(file, in: app), + let share = openFileEntryContextMenu( + for: file, + expectedAction: "Share / Export", + in: app + ), + activateMenuAction(share, in: app) + else { + return + } + + let saveToFiles = app.cells.matching( + NSPredicate( + format: "label IN %@", + ["Save to Files", "存储到“文件”", "存储到文件"] + ) + ).firstMatch + guard saveToFiles.waitForExistence(timeout: 30) else { + attachHierarchy( + named: "System activity presentation did not expose Save to Files", + from: app + ) + XCTFail("system activity presentation to expose Save to Files") + return + } + + if dismissShareSheetIfNeeded(in: app) { + returnToHost(in: app) + } else { + relaunchAndBoot(app) + } + let filesButton = app.buttons["PocketRootHost.files"] + XCTAssertTrue(filesButton.waitForExistence(timeout: 10)) + filesButton.tap() + let persistedFile = app.descendants(matching: .any)[ + "PocketRootFiles.entry./root/\(fileName)" + ] + guard revealFileEntry(persistedFile, in: app), + let delete = openFileEntryContextMenu( + for: persistedFile, + expectedAction: "Delete", + in: app + ) + else { + return + } + delete.tap() + confirmDeletion(of: fileName, in: app) + wait( + for: NSPredicate(format: "exists == false"), + evaluatedWith: persistedFile, + timeout: 30 + ) + shutdownRuntime(in: app) + } + func testPTYCommandCreatesFileVisibleInFiles() { let app = launchAndBoot() @@ -282,7 +361,9 @@ final class PocketRootHostAppUITests: XCTestCase { ) else { return } - share.tap() + guard activateMenuAction(share, in: app) else { + return + } let saveToFiles = app.cells.matching( NSPredicate( @@ -1357,6 +1438,30 @@ final class PocketRootHostAppUITests: XCTestCase { .tap() } + private func activateMenuAction( + _ action: XCUIElement, + in app: XCUIApplication + ) -> Bool { + guard let frames = waitForInteractionFrames( + of: action, + in: app, + timeout: 5 + ) else { + attachHierarchy( + named: "Menu action did not expose a usable frame", + from: app + ) + XCTFail("menu action \(action.label) to expose a usable frame") + return false + } + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + return true + } + private func waitForInteractionFrames( of element: XCUIElement, in app: XCUIApplication, diff --git a/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift b/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift index 9decf25..7c763b1 100644 --- a/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift +++ b/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift @@ -164,22 +164,19 @@ public struct PocketRootFileBrowserView: View { } } } - .sheet( - item: $sharePayload, - onDismiss: { - if let payload = sharePayload { - model.removeExport(payload) - sharePayload = nil - } - } - ) { payload in + .popover(item: $sharePayload) { payload in PocketRootActivityView( url: payload.url, completion: { - model.removeExport(payload) sharePayload = nil } ) + // UIActivityViewController must be presented in a popover on iPad. + // Compact-width devices retain the familiar modal share sheet. + .presentationCompactAdaptation(.sheet) + .onDisappear { + model.removeExport(payload) + } } .task { await model.loadIfNeeded() diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index ee0a0f1..b273c34 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1173,6 +1173,12 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift" ) .binread + file_browser = + REPOSITORY_ROOT + .join( + "Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" + ) + .binread host_source = REPOSITORY_ROOT .join("Examples/PocketRootHostApp/Sources/HostApp.swift") @@ -1200,6 +1206,11 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes ui_test, "try? activityView.snapshot()" assert_includes ui_test, "tapOutsideSnapshotFrame(" refute_includes ui_test, "tapOutsideCurrentFrame" + assert_includes file_browser, '.popover(item: $sharePayload)' + assert_includes file_browser, ".presentationCompactAdaptation(.sheet)" + assert_includes file_browser, ".onDisappear {" + assert_includes file_browser, "model.removeExport(payload)" + refute_includes file_browser, ".sheet(\n item: $sharePayload" assert_includes( ui_test, '"FullDocumentManagerViewControllerNavigationBar"' @@ -1237,7 +1248,9 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes ui_test, "openedHostDestination" assert_includes ui_test, '"DOC.sidebar.item.On My iPad"' assert_includes ui_test, '"PocketRoot Host, Actions Menu"' - assert_equal 3, ui_test.scan("guard openCreationDialog(").length + assert_equal 4, ui_test.scan("guard openCreationDialog(").length + assert_includes ui_test, "testFileExportUsesPlatformActivityPresentation" + assert_equal 2, ui_test.scan("activateMenuAction(share, in: app)").length assert_includes ui_test, "validated physical coordinate" refute_includes ui_test, 'app.buttons["New File"].tap()' refute_includes ui_test, 'app.buttons["New Folder"].tap()' diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index adec5a8..47ebf14 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -459,6 +459,7 @@ fi if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'testSystemFileImportAndShareExportRoundTrip' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'testFileExportUsesPlatformActivityPresentation' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'Save to Files' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'revealFileEntry' "$HOST_UI_TESTS" \ || ! grep -Fq -- '"PocketRootFiles.list"' "$HOST_UI_TESTS" \ @@ -485,6 +486,7 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'Always synthesize through the application using captured values' "$HOST_UI_TESTS" \ || grep -Fq -- 'currentLocalLocation.tap()' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'attachHierarchy(' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'activateMenuAction(share, in: app)' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'underlying host geometry' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'for attempt in 0..<2' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'relaunchAndBoot(app)' "$HOST_UI_TESTS" \ @@ -495,6 +497,9 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'try? activityView.snapshot()' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'tapOutsideSnapshotFrame(' "$HOST_UI_TESTS" \ || grep -Fq -- 'tapOutsideCurrentFrame' "$HOST_UI_TESTS" \ + || ! grep -Fq -- '.popover(item: $sharePayload)' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || ! grep -Fq -- '.presentationCompactAdaptation(.sheet)' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || grep -Fq -- '.sheet(' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ || ! grep -Fq -- 'LSSupportsOpeningDocumentsInPlace: true' "$HOST_PROJECT_SPEC" \ || ! grep -Fq -- 'UIFileSharingEnabled: true' "$HOST_PROJECT_SPEC"; then echo "Host App UI smoke is missing the system file transfer closure." >&2 From 140ee3bbc1be448fc0fb9da71063cf4e705afc7e Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 18:53:26 +0800 Subject: [PATCH 4/5] Complete system file export handoff --- CHANGELOG.en.md | 14 +- CHANGELOG.md | 10 +- .../experimental-v0.2.0/COMPOSITION.json | 4 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +- .../UITests/PocketRootHostAppUITests.swift | 48 ++++++- .../Public/PocketRootFileBrowserView.swift | 132 ++++++++++++++---- Tests/Scripts/ReleaseComplianceTests.rb | 12 +- Tests/Scripts/RuntimeSmokeScriptTests.sh | 9 +- 9 files changed, 187 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index d74b26a..a4771e7 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -43,12 +43,14 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit `UIActivityViewController` in a popover on iPad and adapting to a sheet only in compact-width environments such as iPhone. This avoids the former iPad modal path getting stuck with a dimmed background while the share content - remains below the screen. The temporary export directory is removed after - the presentation disappears whether the user completes, cancels, or taps - outside the share UI. A dedicated iPad export UI smoke now verifies the - platform activity UI and `Save to Files` action without depending on system - file import, and the system round-trip activates its share menu action - through a freshly validated physical frame. + remains below the screen. The temporary export directory is now removed by + the `UIActivityViewController` completion callback, so it is not deleted + while the popover hands the URL to the `Save to Files` document picker; it + is still cleaned up after completion, cancellation, or dismissal. A + dedicated iPad export UI smoke now verifies the platform activity UI, the + `Save to Files` action, and the actual document-picker handoff without + depending on system file import, while the system round-trip activates its + share menu action through a freshly validated physical frame. - Tightened the untagged `v0.2.0` source-candidate audit: `--allow-source-blocked` accepts only final source-release authorization as unsatisfied, and fails on any pinned-gate-set, NOTICE, license, public API diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd04ae..567032a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,10 +29,12 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 未完成转换时保留层级证据,不会要求正在消失的系统 cell 重新解析并执行自身动作。 - Files 导出现在按 UIKit 平台契约在 iPad 使用 popover 呈现 `UIActivityViewController`,并仅在 iPhone 等紧凑宽度环境适配为 sheet;这避免 iPad - 在先前的模态 sheet 路径中只显示变暗背景、分享内容仍停留在屏幕下方。无论用户完成、 - 取消还是点按外部关闭分享界面,临时导出目录都会随展示内容退场后清理。新增独立的 - iPad 导出 UI smoke,不依赖系统文件导入即可验证平台分享界面和 `Save to Files` 动作; - 系统 round-trip 的分享菜单动作也改为通过重新校验的物理 frame 触发。 + 在先前的模态 sheet 路径中只显示变暗背景、分享内容仍停留在屏幕下方。临时导出目录 + 改由 `UIActivityViewController` 完成回调清理,不会在 popover 为 `Save to Files` 交接 + 系统文件保存器时提前删除;完成、取消或关闭分享流程后仍会清理。新增独立的 iPad + 导出 UI smoke,不依赖系统文件导入即可验证平台分享界面、`Save to Files` 动作以及 + 文件保存器确实打开;系统 round-trip 的分享菜单动作也改为通过重新校验的物理 frame + 触发。 - 收紧 `v0.2.0` 未打 tag 源码候选审计:`--allow-source-blocked` 只接受最终源码发布 授权这一项尚未满足,固定门禁集合、NOTICE、许可证、公开 API 状态或源码边界漂移 都会失败;CI 和受信 tag 工作流只上传带 commit、archive SHA-256、文件统计和精确 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 9f3b719..8a6c518 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -91,7 +91,7 @@ "Sources/PocketRootTerminal/Bridge/TerminalBridge.swift": "b0850236d896a517258dc310c30f2f63dfaf4450cb7ff732a3ba3414c2da2002", "Sources/PocketRootTerminal/Public/PocketRootCommandTerminalSession.swift": "5eaa24f24efb46de1482029b3c5f2ce33942a9e961249fbdaeb728eabe92257a", "Sources/PocketRootTerminal/Public/PocketRootFileBrowser.swift": "2f74c5a311899995f8a941402ea5e3f7cad07a922fb341ff183e1b0795944801", - "Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift": "cc3a66da920eadba530cb40aed76469023426a9bf1d6e0d05e2a2b5c0d584473", + "Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift": "9a48150a7d3beb9cfed4ea882d8789891e9145813be6f551ab31ebdb0e920135", "Sources/PocketRootTerminal/Public/PocketRootTerminalCommandExecutor.swift": "e9deacbb10e81adc412147a1a1b0a6f7896acfa2be4ef1b1a855e9cb01b09e61", "Sources/PocketRootTerminal/Public/PocketRootTerminalConfiguration.swift": "2207a7c358517ddf2d8791019b39ce4f6b27203b0763046e40f825384ab08d14", "Sources/PocketRootTerminal/Public/PocketRootTerminalTheme.swift": "a0f67e8d79b76465bb6b64d4ea3f48b886bc6a5d2d85a59669b82699e0e0649b", @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "2a518e063f4a8ccaa4b97d8851821cd77f5eaf036a512c7af08bf242591e84c2", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "1b4c523381bce7e9b2872df2a9f2c241369b22b57706d9c56c79d2a99bcf528a", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "9d1c2a16adbd6918ea86d36808bfe360047ecb222d38afca2efd8db2d2eaf373", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index caec26f..28b55c0 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/184e4a4f2472396b778a344215e90f7697cedaf1c6b9e0847e18ba3a6807db49", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/865480ca0fe73a96f785312eccace82a793dc12069f4dfbbc660751de46c8fdc", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 1d221df..0acccd7 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -184e4a4f2472396b778a344215e90f7697cedaf1c6b9e0847e18ba3a6807db49 COMPOSITION.json +865480ca0fe73a96f785312eccace82a793dc12069f4dfbbc660751de46c8fdc COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -6c1a8254582b5721fd4f0973a0bb3e54beb25fae9d63ad7824e3298ec85f04ed SBOM.spdx.json +04e62264420edca955d2b91a23e413867d4ca769ac1b91a45e9d009d570f9899 SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 5f5ed19..95def97 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -229,11 +229,31 @@ final class PocketRootHostAppUITests: XCTestCase { return } - if dismissShareSheetIfNeeded(in: app) { - returnToHost(in: app) - } else { - relaunchAndBoot(app) + guard activateMenuAction(saveToFiles, in: app), + waitForDocumentPickerPresentation(in: app, timeout: 30) + else { + attachHierarchy( + named: "Save to Files did not open the document picker", + from: app + ) + XCTFail("Save to Files to open the document picker") + return } + let cancel = app.buttons.matching( + NSPredicate(format: "label IN %@", ["Cancel", "取消"]) + ).firstMatch + if let frames = waitForInteractionFrames( + of: cancel, + in: app, + timeout: 10 + ) { + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + } + relaunchAndBoot(app) let filesButton = app.buttons["PocketRootHost.files"] XCTAssertTrue(filesButton.waitForExistence(timeout: 10)) filesButton.tap() @@ -1549,6 +1569,26 @@ final class PocketRootHostAppUITests: XCTestCase { return currentHostDocuments.exists || hostDestination.exists } + private func waitForDocumentPickerPresentation( + in app: XCUIApplication, + timeout: TimeInterval + ) -> Bool { + let navigationBar = app.navigationBars[ + "FullDocumentManagerViewControllerNavigationBar" + ] + let cancel = app.buttons.matching( + NSPredicate(format: "label IN %@", ["Cancel", "取消"]) + ).firstMatch + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if navigationBar.exists || cancel.exists { + return true + } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } + return navigationBar.exists || cancel.exists + } + private func attachHierarchy( named name: String, from app: XCUIApplication diff --git a/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift b/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift index 7c763b1..b872129 100644 --- a/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift +++ b/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift @@ -164,18 +164,17 @@ public struct PocketRootFileBrowserView: View { } } } - .popover(item: $sharePayload) { payload in - PocketRootActivityView( - url: payload.url, - completion: { - sharePayload = nil - } - ) - // UIActivityViewController must be presented in a popover on iPad. - // Compact-width devices retain the familiar modal share sheet. - .presentationCompactAdaptation(.sheet) - .onDisappear { - model.removeExport(payload) + .background { + if let payload = sharePayload { + PocketRootActivityPresenter( + payload: payload, + completion: { + model.removeExport(payload) + if sharePayload?.id == payload.id { + sharePayload = nil + } + } + ) } } .task { @@ -909,25 +908,110 @@ private struct PocketRootSharePayload: Identifiable, Sendable { } @available(iOS 18.0, *) -private struct PocketRootActivityView: UIViewControllerRepresentable { - let url: URL +private struct PocketRootActivityPresenter: UIViewControllerRepresentable { + let payload: PocketRootSharePayload let completion: () -> Void - func makeUIViewController(context: Context) -> UIActivityViewController { - let controller = UIActivityViewController( - activityItems: [url], - applicationActivities: nil - ) - controller.completionWithItemsHandler = { _, _, _, _ in - completion() - } + func makeUIViewController( + context: Context + ) -> PocketRootActivityPresenterViewController { + let controller = PocketRootActivityPresenterViewController() + controller.configure(payload: payload, completion: completion) return controller } func updateUIViewController( - _ uiViewController: UIActivityViewController, + _ uiViewController: PocketRootActivityPresenterViewController, context: Context - ) {} + ) { + uiViewController.configure(payload: payload, completion: completion) + } +} + +@available(iOS 18.0, *) +private final class PocketRootActivityPresenterViewController: + UIViewController, + UIPopoverPresentationControllerDelegate +{ + private var payloadID: UUID? + private var exportURL: URL? + private var presentationCompletion: (() -> Void)? + private var isPresentingActivity = false + private var didFinishActivity = false + + override func loadView() { + let view = UIView() + view.backgroundColor = .clear + self.view = view + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + presentActivityIfNeeded() + } + + func configure( + payload: PocketRootSharePayload, + completion: @escaping () -> Void + ) { + if payloadID != payload.id { + payloadID = payload.id + exportURL = payload.url + didFinishActivity = false + } + presentationCompletion = completion + presentActivityIfNeeded() + } + + private func presentActivityIfNeeded() { + guard viewIfLoaded?.window != nil, + presentedViewController == nil, + !isPresentingActivity, + !didFinishActivity, + let exportURL + else { + return + } + + isPresentingActivity = true + let activity = UIActivityViewController( + activityItems: [exportURL], + applicationActivities: nil + ) + activity.modalPresentationStyle = .popover + activity.completionWithItemsHandler = { [weak self] _, _, _, _ in + self?.finishActivity() + } + if let popover = activity.popoverPresentationController { + let bounds = view.bounds + popover.sourceView = view + popover.sourceRect = CGRect( + x: bounds.midX, + y: bounds.midY, + width: 1, + height: 1 + ) + popover.permittedArrowDirections = [] + popover.delegate = self + } + present(activity, animated: true) + } + + func popoverPresentationControllerDidDismissPopover( + _ popoverPresentationController: UIPopoverPresentationController + ) { + finishActivity() + } + + private func finishActivity() { + guard !didFinishActivity else { + return + } + didFinishActivity = true + isPresentingActivity = false + presentationCompletion?() + presentationCompletion = nil + } } private enum PocketRootFileTransferUIError: LocalizedError { diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index b273c34..8b3f790 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1206,10 +1206,14 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes ui_test, "try? activityView.snapshot()" assert_includes ui_test, "tapOutsideSnapshotFrame(" refute_includes ui_test, "tapOutsideCurrentFrame" - assert_includes file_browser, '.popover(item: $sharePayload)' - assert_includes file_browser, ".presentationCompactAdaptation(.sheet)" - assert_includes file_browser, ".onDisappear {" + assert_includes file_browser, "PocketRootActivityPresenter(" + assert_includes file_browser, "activity.modalPresentationStyle = .popover" + assert_includes file_browser, "popover.sourceView = view" + assert_includes file_browser, "popover.permittedArrowDirections = []" + assert_includes file_browser, "popoverPresentationControllerDidDismissPopover(" assert_includes file_browser, "model.removeExport(payload)" + refute_includes file_browser, '.popover(item: $sharePayload)' + refute_includes file_browser, ".presentationCompactAdaptation(.sheet)" refute_includes file_browser, ".sheet(\n item: $sharePayload" assert_includes( ui_test, @@ -1250,6 +1254,8 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes ui_test, '"PocketRoot Host, Actions Menu"' assert_equal 4, ui_test.scan("guard openCreationDialog(").length assert_includes ui_test, "testFileExportUsesPlatformActivityPresentation" + assert_includes ui_test, "waitForDocumentPickerPresentation(in: app, timeout: 30)" + assert_includes ui_test, 'XCTFail("Save to Files to open the document picker")' assert_equal 2, ui_test.scan("activateMenuAction(share, in: app)").length assert_includes ui_test, "validated physical coordinate" refute_includes ui_test, 'app.buttons["New File"].tap()' diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index 47ebf14..46d0377 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -461,6 +461,8 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'testSystemFileImportAndShareExportRoundTrip' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'testFileExportUsesPlatformActivityPresentation' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'Save to Files' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'waitForDocumentPickerPresentation(in: app, timeout: 30)' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'Save to Files to open the document picker' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'revealFileEntry' "$HOST_UI_TESTS" \ || ! grep -Fq -- '"PocketRootFiles.list"' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'if element.isEnabled' "$HOST_UI_TESTS" \ @@ -497,8 +499,11 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'try? activityView.snapshot()' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'tapOutsideSnapshotFrame(' "$HOST_UI_TESTS" \ || grep -Fq -- 'tapOutsideCurrentFrame' "$HOST_UI_TESTS" \ - || ! grep -Fq -- '.popover(item: $sharePayload)' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ - || ! grep -Fq -- '.presentationCompactAdaptation(.sheet)' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || ! grep -Fq -- 'PocketRootActivityPresenter(' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || ! grep -Fq -- 'activity.modalPresentationStyle = .popover' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || ! grep -Fq -- 'popover.sourceView = view' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || ! grep -Fq -- 'popoverPresentationControllerDidDismissPopover(' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ + || ! grep -Fq -- 'model.removeExport(payload)' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ || grep -Fq -- '.sheet(' "$ROOT_DIR/Sources/PocketRootTerminal/Public/PocketRootFileBrowserView.swift" \ || ! grep -Fq -- 'LSSupportsOpeningDocumentsInPlace: true' "$HOST_PROJECT_SPEC" \ || ! grep -Fq -- 'UIFileSharingEnabled: true' "$HOST_PROJECT_SPEC"; then From 52b5f0d9413001c645e7e13a8ca457bd6d06df33 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 19:48:11 +0800 Subject: [PATCH 5/5] Avoid transient document picker cancel query --- .../experimental-v0.2.0/COMPOSITION.json | 2 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +-- .../UITests/PocketRootHostAppUITests.swift | 29 +++---------------- Tests/Scripts/ReleaseComplianceTests.rb | 2 ++ Tests/Scripts/RuntimeSmokeScriptTests.sh | 2 ++ 6 files changed, 12 insertions(+), 29 deletions(-) diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 8a6c518..24d4bfa 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "1b4c523381bce7e9b2872df2a9f2c241369b22b57706d9c56c79d2a99bcf528a", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "c7f18083694ba05706cdec2be8286acbf5e1e0d7b1f4479f84733199dd02fefb", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "9d1c2a16adbd6918ea86d36808bfe360047ecb222d38afca2efd8db2d2eaf373", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index 28b55c0..6486855 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/865480ca0fe73a96f785312eccace82a793dc12069f4dfbbc660751de46c8fdc", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/05495d05d69b4ddec325abb9b712c6bd632df73213c7c0ac51db6b2f036a6ba4", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 0acccd7..80dbcc7 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -865480ca0fe73a96f785312eccace82a793dc12069f4dfbbc660751de46c8fdc COMPOSITION.json +05495d05d69b4ddec325abb9b712c6bd632df73213c7c0ac51db6b2f036a6ba4 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -04e62264420edca955d2b91a23e413867d4ca769ac1b91a45e9d009d570f9899 SBOM.spdx.json +888fffde64e4eed026296939334500cdc52d98694708e4edcb2a4130916bfe37 SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 95def97..7e1687d 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -239,20 +239,9 @@ final class PocketRootHostAppUITests: XCTestCase { XCTFail("Save to Files to open the document picker") return } - let cancel = app.buttons.matching( - NSPredicate(format: "label IN %@", ["Cancel", "取消"]) - ).firstMatch - if let frames = waitForInteractionFrames( - of: cancel, - in: app, - timeout: 10 - ) { - tapFrame( - frames.elementFrame, - in: frames.appFrame, - using: app - ) - } + // The document picker owns its transient Cancel element. Restart the + // host after proving the handoff instead of resolving that system + // button while the activity controller is completing its transition. relaunchAndBoot(app) let filesButton = app.buttons["PocketRootHost.files"] XCTAssertTrue(filesButton.waitForExistence(timeout: 10)) @@ -1576,17 +1565,7 @@ final class PocketRootHostAppUITests: XCTestCase { let navigationBar = app.navigationBars[ "FullDocumentManagerViewControllerNavigationBar" ] - let cancel = app.buttons.matching( - NSPredicate(format: "label IN %@", ["Cancel", "取消"]) - ).firstMatch - let deadline = Date().addingTimeInterval(timeout) - while Date() < deadline { - if navigationBar.exists || cancel.exists { - return true - } - RunLoop.current.run(until: Date().addingTimeInterval(0.2)) - } - return navigationBar.exists || cancel.exists + return navigationBar.waitForExistence(timeout: timeout) } private func attachHierarchy( diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 8b3f790..6aacd3c 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1256,6 +1256,8 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes ui_test, "testFileExportUsesPlatformActivityPresentation" assert_includes ui_test, "waitForDocumentPickerPresentation(in: app, timeout: 30)" assert_includes ui_test, 'XCTFail("Save to Files to open the document picker")' + assert_includes ui_test, "return navigationBar.waitForExistence(timeout: timeout)" + assert_includes ui_test, "The document picker owns its transient Cancel element" assert_equal 2, ui_test.scan("activateMenuAction(share, in: app)").length assert_includes ui_test, "validated physical coordinate" refute_includes ui_test, 'app.buttons["New File"].tap()' diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index 46d0377..0e0e543 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -463,6 +463,8 @@ if ! grep -Fq -- 'pocketroot-system-file-ui-fixture.txt' "$HOST_APP_SOURCE" \ || ! grep -Fq -- 'Save to Files' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'waitForDocumentPickerPresentation(in: app, timeout: 30)' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'Save to Files to open the document picker' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'return navigationBar.waitForExistence(timeout: timeout)' "$HOST_UI_TESTS" \ + || ! grep -Fq -- 'The document picker owns its transient Cancel element' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'revealFileEntry' "$HOST_UI_TESTS" \ || ! grep -Fq -- '"PocketRootFiles.list"' "$HOST_UI_TESTS" \ || ! grep -Fq -- 'if element.isEnabled' "$HOST_UI_TESTS" \