Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apple/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apple/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ let package = Package(
.library(name: "LogseqModel", type: .dynamic, targets: ["LogseqModel"]),
],
dependencies: [
.package(url: "ssh://git@github.com/logseq/lui.git", revision: "4281a5160ca9833d3b71901071dcb40228dcc36d"),
.package(url: "ssh://git@github.com/logseq/lui.git", revision: "71964a686a8fb1d0878e2fe122f28bd155e657ef"),
.package(url: "https://github.com/gonzalezreal/swiftui-math", from: "0.1.0"),
.package(url: "https://github.com/appstefan/highlightswift.git", from: "1.1.0")
],
Expand Down
114 changes: 114 additions & 0 deletions apple/Tests/UI/query-disclosure-lifecycle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import SwiftUI
import UIKit
import Vision
@testable import Logseq
@testable import LUIAppleBackend

@MainActor private final class QueryDisclosureEffects: LGEffectExecuting {
var snapshot: ((Bool) throws -> String)!
var expansions: [Bool] = []
func execute(_ effect: LGEffect) async -> LGEffectResolution {
guard effect.kind == "result-view" else {
return LGEffectResolution(succeeded: true, message: "", output: .discard)
}
do {
let input = try JSONSerialization.jsonObject(with: Data(effect.text.utf8)) as! [String: Any]
let expanded = input["expanded"] as! Bool
expansions.append(expanded)
return LGEffectResolution(succeeded: true, message: try snapshot(expanded))
} catch { return LGEffectResolution(succeeded: false, message: String(describing: error)) }
}
}

@main struct QueryDisclosureLifecycleCheck {
@MainActor static func main() async throws {
for zoomed in [false, true] {
func snapshot(_ expanded: Bool) throws -> String {
let rows: [[String: Any]] = expanded ? (0..<2).map { index in
["block": ["uuid": "result-\(index)", "title": "Project item \(index + 1)", "pageId": "page"]]
} : []
var query: [String: Any] = ["ownerUuid": "query-owner", "featureType": "query-result",
"kind": expanded ? "simple" : "deferred", "expanded": expanded, "rows": rows, "views": []]
if expanded {
query["views"] = [["uuid": "query-owner:default", "name": "All", "type": "list", "rows": rows,
"columns": [], "error": NSNull()]]
}
let outline: [[String: Any]] = [["depth": 0, "hasChildren": false, "isCollapsed": false,
"block": ["uuid": "query-owner", "title": "Project query", "pageId": "page", "query": query]]]
var result: [String: Any] = ["selectedGraphId": "query-disclosure", "graphName": "Queries"]
if zoomed {
result["nodeRoutes"] = [["uuid": "query-owner", "page": ["uuid": "page", "title": "Workspace"],
"outlinerRows": outline]]
} else { result["outlinerRows"] = outline }
return String(decoding: try JSONSerialization.data(withJSONObject: ["ok": true, "result": result]), as: UTF8.self)
}
let effects = QueryDisclosureEffects()
effects.snapshot = snapshot
let runtime = LGRuntime(native: LGCoreNativeCaller(), effectExecutor: effects)
try runtime.applyCoreResponse(snapshot(false))
try runtime.start(platformCode: 2, hostCode: 2, authenticationCode: 3)
try runtime.applyHostUpdate(kind: "graph-loading", payload: "false")
if zoomed { try runtime.applyHostUpdate(kind: "open-node", payload: "\"query-owner\"") }
let backend = runtime.renderer.backend
var frames: [Int: CGRect] = [:]
backend.frameReportingEnabled = true
backend.onFramesReport = { frames = $0 }
func ids(_ id: Int) -> [Int] {
[id] + (backend.model(id: id)?.children ?? backend.extensionModel(id: id)?.children ?? []).flatMap(ids)
}
func find(_ identifier: String) -> Int? {
backend.rootIDs.flatMap(ids).last {
backend.model(id: $0)?.property(.accessibilityIdentifier)?.stringValue == identifier
}
}
func wait(_ message: String, until predicate: () -> Bool) async throws {
for _ in 0..<100 {
if predicate() { return }
try await Task.sleep(for: .milliseconds(20))
}
throw failure(message + "; runtime=" + (runtime.lastError ?? "none"))
}
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = UIWindow(windowScene: scene)
window.rootViewController = UIHostingController(rootView: LGRendererRoot(renderer: runtime.renderer))
window.makeKeyAndVisible()
defer { window.isHidden = true; window.rootViewController = nil; runtime.stop() }
func visibleText(_ phase: String) async throws -> String {
try await Task.sleep(for: .milliseconds(350))
let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
try image.pngData()!.write(to: URL.documentsDirectory.appendingPathComponent("query-disclosure-\(zoomed)-\(phase).png"))
guard let icon = find("query.disclosure.query-owner"), let frame = frames[icon],
abs(frame.width - 10) < 0.5, abs(frame.height - 10) < 0.5 else {
throw failure("The query disclosure must actually render at the block's 10pt icon size")
}
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
try VNImageRequestHandler(cgImage: image.cgImage!, options: [:]).perform([request])
return (request.results ?? []).compactMap { $0.topCandidates(1).first?.string }.joined(separator: " ")
}
try await wait("Query must mount") { find("query.toggle.query-owner") != nil }
let folded = try await visibleText("folded")
guard folded.contains("Project query"), folded.contains("Results"), effects.expansions.isEmpty else {
throw failure("A deferred query must show its native Results label without executing: \(folded)")
}
try backend.performPress(node: find("query.toggle.query-owner")!)
try await wait("Expanding executes and displays results") { find("query.result.query-owner.1") != nil }
guard try await visibleText("expanded").contains("2 results"), effects.expansions == [true] else {
throw failure("Expanded query must display the updated result count")
}
try backend.performPress(node: find("query.toggle.query-owner")!)
try await wait("Folding removes results") { find("query.result.query-owner.0") == nil }
guard try await visibleText("refolded").contains("Results"), effects.expansions == [true, false] else {
throw failure("Folded query must restore its Results label")
}
try backend.performPress(node: find("query.toggle.query-owner")!)
try await wait("Reopening executes again") { effects.expansions == [true, false, true] && find("query.result.query-owner.1") != nil }
}
print("PASS: deferred queries show native Results labels, expand, update counts, fold and rerun in pages and zoomed blocks")
}
private static func failure(_ message: String) -> NSError {
NSError(domain: "QueryDisclosureLifecycleCheck", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
}
}
8 changes: 8 additions & 0 deletions docs/agent-guide/016-mobile-interaction-regressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,11 @@ The dedicated physical warm control completed ten row/navigation updates while b
The old build was also exercised with stable row identities changing between bold text, quotes, formulas, highlighted code, local images and node references during six real native pushes and six pops. Screenshot inspection caught an invalid image-type fixture and then an unfinished image decode; the final fixture uses `png` and waits for the rendered orange image before starting navigation. The saved screenshot confirms actual image, formula, code and quote content. All 42 content submissions occur while a native transition coordinator exists. The final merged stdout/stderr run completes at 8.9337 seconds without the matching fault or a cycle warning (`/tmp/logseq-lifecycle-mixed-verified-image-console.log`). A physical mixed-content attempt was rejected before launch because the phone was locked; no mixed-content device pass is claimed.

The follow-up simulator sources, verified source-hash manifest, screenshot and sanitized controls are archived at `/tmp/logseq-lifecycle-mixed-followup-sources.zip` (266,883 bytes; SHA-256 `e91119bb9c7f411624a3b738b5caf25934e83fd1d603aa778a629909ced72bfe`). This is separate from the earlier lifecycle archive; its current simulator manifest must not be used to identify the earlier physical binary. No production code or app installation changed in this crash-audit follow-up. The exact crash has not been reproduced and its causal repair remains open.

### Invisible collapsed query disclosure regression

The user's S1 screenshot was reproduced with the original demo graph's mobile-session response: the query and its 44-point Results button exist, but its native label and icon are absent. Commit `72ae7c2` changed the disclosure to button content children for an independently sized icon. The Apple renderer still rendered only the button's text/icon properties and ignored content children, leaving the default collapsed query's sole entry point blank. Earlier native checks verified the retained icon model and expanded results without asserting that the initial Results label was actually drawn.

LUI `71964a68` renders Button/ToggleButton label children through the existing retained-node renderer, excludes context-menu metadata, and keeps the normal text/icon fallback. Its native runtime check fails on the old renderer with `Fallback Plain`, then passes custom labels, child text updates, metadata exclusion and fallback restoration (`/tmp/lui-button-children-native-{red,green}.log`). The consumer pins the same revision in SwiftPM and opam.

The new `apple/Tests/UI/query-disclosure-lifecycle.swift` first fails because Results is absent from the screenshot. After the fix, it passes actual rendered Results text and 10-point glyph frames, lazy expansion, updated counts, folding and re-execution on ordinary pages and zoomed query blocks (`/tmp/logseq-query-disclosure-lifecycle-{red,green}.log`). A separate real-session core round trip covers simple/advanced queries on both hosts, including zoom-in; it confirms that execution itself was intact. The full core gate passes 1048 tests in 66.724 seconds (`/tmp/logseq-query-disclosure-core-final.log`). The rebuilt complete iOS query workflow also passes lists/selection, saved views, horizontal tables, cell editing, filter/sort sheets, gallery, search and local errors (`/tmp/logseq-query-fixed-workflow.log`). This fixes the missing query entry point; it does not establish a repair of the unrelated TestFlight crash.
2 changes: 1 addition & 1 deletion logseq.opam
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pin-depends: [
["melange-transit-core.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"]
["melange-transit-native.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"]
["melange-transit-melange.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"]
["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#4281a5160ca9833d3b71901071dcb40228dcc36d"]
["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#71964a686a8fb1d0878e2fe122f28bd155e657ef"]
["drive.dev" "git+https://github.com/logseq/drive.git#dd735a65ed525d1c36db38ac8e8114aa81d314a1"]
["ocaml-signal.0.1.0" "git+https://github.com/logseq/ocaml-signal.git#02e18cdc5e0ad672aa223f638eaa3ba52fc9421e"]
["mldoc.dev" "git+https://github.com/logseq/mldoc.git#f11230c367964af2b575524d00779cfb1ffc7147"]
Expand Down
43 changes: 43 additions & 0 deletions shared/test/logseq/query_block_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,50 @@ let collapsed_queries_do_not_evaluate_until_opened () =
member "action" (Json.from_string payload) = `String "setExpanded" | _ -> false)
(App.model app).pending_effects))) [App_test.ios_backend; App_test.kotlin_backend]

let results_disclosure_executes_real_session () =
List.iter (fun backend ->
List.iter (fun source ->
let session, _ = make_session (fixture source) in
ignore (Rpc_test.configure_plain_session session);
List.iter (fun zoom ->
ignore (dispatch session "resultView" (Json.to_string (`Assoc ["ownerUuid", `String owner;
"featureType", `String "query-result"; "action", `String "setExpanded"; "expanded", `Bool false])));
if zoom then ignore (dispatch session "openNode" (Json.to_string (`Assoc ["uuid", `String owner])));
with_ui backend (Session.snapshot_visible session) (fun app ->
let runtime = Lui_app.runtime app in
let find id = App_test.descendant_with_identifier runtime (Lui_app.root_node app) id in
let toggle () =
let button = find ("query.toggle." ^ owner) in
check ~msg:"the query disclosure remains reachable" (button <> -1);
App_test.dispatch app (Lui_protocol.Press button);
App_test.flush app;
let eff = List.find (function Model.ResultViewEffect _ -> true | _ -> false)
(App.model app).pending_effects in
let id = Model.effect_id eff in
App_test.send app (Model.DequeueEffect id);
let response = Core_effect_test.execute session "result-view"
["text", (match eff with Model.ResultViewEffect (_, _, payload) -> `String payload | _ -> assert false)] in
check_eq ~msg:(Json.to_string response) (member "ok" response) (`Bool true);
App_test.send app (Model.ResolveEffect (id, true, Json.to_string response));
(match Response_snapshot.decode_response (Json.to_string response) with
| Ok projection -> App_test.send app (Model.ApplyCoreSnapshot projection)
| Error message -> fail message);
App_test.flush app in
check_eq (find ("query.result." ^ owner ^ ".0")) (-1);
toggle ();
check ~msg:("real query results must mount after expansion: " ^ source)
(find ("query.result." ^ owner ^ ".0") <> -1);
toggle ();
check_eq ~msg:"folded results unmount after a real core response"
(find ("query.result." ^ owner ^ ".0")) (-1);
toggle ();
check ~msg:"queries can run again after folding"
(find ("query.result." ^ owner ^ ".0") <> -1))) [false; true])
["#Project"; "{:query [:find (pull ?b [*]) :where [?b :user.property/score ?s] [(> ?s 1)]]}"])
[App_test.ios_backend; App_test.kotlin_backend]

let cases = [
case "results disclosure executes and folds real simple and advanced queries" results_disclosure_executes_real_session;
case "queries stay unevaluated until expanded and refresh when reopened" collapsed_queries_do_not_evaluate_until_opened;
case "result lists render breadcrumbs and independently folded children" result_list_breadcrumbs_and_local_children;
case "cross-page results retain editing and select bar actions" cross_page_result_actions;
Expand Down
Loading