diff --git a/AGENTS.md b/AGENTS.md
index ef38f27..4e054fa 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -53,6 +53,6 @@ KSPlayer is intentionally pinned to `bb6d367c02f3247ef3f328dc0cfea34e77aef7c5` f
- UI tests live in `swiftchanUITests/`. The shared scheme runs `BrowsingUITests` for following/editing generals, link validation, reading restoration, and unread navigation. These tests use a debug-only in-memory favorites store (`--ui-testing`); Thread UI tests opt into a deterministic debug-only fixture with `--ui-thread-fixture`; older live-network UI tests remain excluded.
- Add focused regressions for behavior fixes. Prefer fixtures and injected loaders over live network dependencies.
- GitHub Actions builds and runs unit and focused UI tests for pull requests.
-- The build/test step has a 35-minute limit within the 40-minute job. A cold hosted build plus the full UI suite exceeded the earlier 25-minute limit while tests were still passing; keep time for compilation and slower simulator interaction. Gallery paging returns to the post associated with the selected media; test that post after dismissing the gallery instead of expecting the first post to remain onscreen. Favorites export checks must identify the actual `Browse View (Picker)` plus either `DOCPicker.filenameTextField` or Save. Verify the expected backup filename whenever its field is exposed. Captured hosted runs show that Browse can expose the filename without Save, while a folder can expose Save without the filename field.
+- The build/test step has a 35-minute limit within the 40-minute job. A cold hosted build plus the full UI suite exceeded the earlier 25-minute limit while tests were still passing; keep time for compilation and slower simulator interaction. Gallery paging returns to the post associated with the selected media; test that post after dismissing the gallery instead of expecting the first post to remain onscreen. Allow up to 60 seconds for the first Files picker on a cold hosted simulator; captured failures showed its remote view empty after the earlier 20-second wait. Favorites export checks must identify the actual `Browse View (Picker)` plus either `DOCPicker.filenameTextField` or Save. Verify the expected backup filename whenever its field is exposed. Captured hosted runs show that Browse can expose the filename without Save, while a folder can expose Save without the filename field.
- `bundle exec fastlane beta` signs and uploads a TestFlight release, then bumps the build number. Run it only when releasing is requested.
- Certificates are managed with Fastlane match (`make renew_certs` / `make get_certs`).
diff --git a/docs/ipad-testing.md b/docs/ipad-testing.md
index 15e2b66..3d0212d 100644
--- a/docs/ipad-testing.md
+++ b/docs/ipad-testing.md
@@ -32,3 +32,13 @@ The shared workspace review is `../iphone-duo-review/2026-10-04-current/review/`
The follow-up catalog grid review is `../iphone-duo-review/2026-10-04-grid/`. Keep its build 4 clips and validation separate from the earlier build 3 Swiftchan footage.
The full-screen navigation review is `../iphone-duo-review/2026-10-05-fullscreen/`, for build 1.3 (5). Its mini portrait and Pro landscape captures verify the actual full-width thread and Back restoring the scrolled catalog.
+
+## Gallery controls
+
+Build 1.3 (6) opens each gallery page without the image/thumbnail search button or media counter. A single media tap reveals both, and another hides them. The optional thumbnail strip follows the same tap. Paging, zooming, and seeking clear the overlays; the close button and long-press media actions remain available when not zoomed. Check preview enabled and disabled, double-tap zoom, vertical paging, and returning to the selected post.
+
+The gallery UI regressions use invented image artwork and a local silent WebM fixture. For native recordings, run `testPrepareGalleryCaptureOrientation` with `TEST_RUNNER_GALLERY_CAPTURE_ORIENTATION=portrait` or `landscapeLeft` before starting the recorder; the walkthrough also asserts the app's actual window orientation before opening the gallery. Set `TEST_RUNNER_DUO_CAPTURE=1` to pause at capture markers. Leave the orientation override unset when preserving a verified Duo pose.
+
+With `xcodebuild`, the `TEST_RUNNER_` prefix is stripped when forwarding these variables to XCTest: the test reads `GALLERY_CAPTURE_ORIENTATION` and `DUO_CAPTURE`. Orientation assertions apply only to gallery captures. Record simulator walkthroughs sequentially; concurrent capture runs against shared test products produced stale display frames and unstable automation sessions. The build 6 gallery review is `../iphone-duo-review/2026-10-05-gallery/`.
+
+During iPad video presentation, XCTest can report a zero-sized accessibility window while the gallery controls remain present. Check the real app window before opening media, then verify every native PNG's orientation in the capture harness. Do not treat a zero accessibility frame as the video display's geometry.
diff --git a/docs/iphone-duo-testing.md b/docs/iphone-duo-testing.md
index 43f6922..05b64d1 100644
--- a/docs/iphone-duo-testing.md
+++ b/docs/iphone-duo-testing.md
@@ -98,3 +98,13 @@ Read the complete relevant Apple documents and upstream tool instructions when c
- [Apple: Interacting with your app in the iOS or iPadOS Simulator](https://developer.apple.com/documentation/xcode/interacting-with-your-app-in-the-ios-or-ipados-simulator)
- [MobileBuildMCP 2.7.0](https://github.com/getsentry/MobileBuildMCP/releases/tag/v2.7.0) and [2.7.1](https://github.com/getsentry/MobileBuildMCP/releases/tag/v2.7.1)
- [Agent Device foldable-panel ADR](https://github.com/callstack/agent-device/blob/main/docs/adr/0025-foldable-apple-panels.md), especially the 2026-09-22 headless amendment and the accepted quarter-turn evidence gap
+
+## Gallery controls
+
+Build 1.3 (6) opens each gallery page without the image/thumbnail search button or media counter. A single media tap reveals both, and another hides them. The optional thumbnail strip follows the same tap. Paging, zooming, and seeking clear the overlays; the close button and long-press media actions remain available when not zoomed. Check preview enabled and disabled, double-tap zoom, vertical paging, and returning to the selected post.
+
+The gallery UI regressions use invented image artwork and a local silent WebM fixture. For native recordings, run `testPrepareGalleryCaptureOrientation` with `TEST_RUNNER_GALLERY_CAPTURE_ORIENTATION=portrait` or `landscapeLeft` before starting the recorder; the walkthrough also asserts the app's actual window orientation before opening the gallery. Set `TEST_RUNNER_DUO_CAPTURE=1` to pause at capture markers. Leave the orientation override unset when preserving a verified Duo pose.
+
+With `xcodebuild`, the `TEST_RUNNER_` prefix is stripped when forwarding these variables to XCTest: the test reads `GALLERY_CAPTURE_ORIENTATION` and `DUO_CAPTURE`. Orientation assertions apply only to gallery captures. Record simulator walkthroughs sequentially; concurrent capture runs against shared test products produced stale display frames and unstable automation sessions. The build 6 gallery review is `../iphone-duo-review/2026-10-05-gallery/`.
+
+During iPad video presentation, XCTest can report a zero-sized accessibility window while the gallery controls remain present. Check the real app window before opening media, then verify every native PNG's orientation in the capture harness. Do not treat a zero accessibility frame as the video display's geometry.
diff --git a/swiftchan.xcodeproj/project.pbxproj b/swiftchan.xcodeproj/project.pbxproj
index 3e6674a..8db056c 100644
--- a/swiftchan.xcodeproj/project.pbxproj
+++ b/swiftchan.xcodeproj/project.pbxproj
@@ -1101,7 +1101,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 5;
+ CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_ASSET_PATHS = "\"swiftchan/Preview Content\"";
DEVELOPMENT_TEAM = 8Q3RG3ULSU;
ENABLE_BITCODE = NO;
@@ -1133,7 +1133,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 5;
+ CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_ASSET_PATHS = "\"swiftchan/Preview Content\"";
DEVELOPMENT_TEAM = 8Q3RG3ULSU;
ENABLE_BITCODE = NO;
diff --git a/swiftchan/Services/ThreadUITestFixture.swift b/swiftchan/Services/ThreadUITestFixture.swift
index 8750d2e..b9d99e1 100644
--- a/swiftchan/Services/ThreadUITestFixture.swift
+++ b/swiftchan/Services/ThreadUITestFixture.swift
@@ -23,13 +23,15 @@
let timestamp = 9_000_000_000 + number
post["tim"] = timestamp
post["filename"] = "collection-study-\(number - id + 1)"
- post["ext"] = ".png"
+ let isVideo = arguments.contains("--ui-video-media-fixture") && number == id + 2
+ post["ext"] = isVideo ? ".webm" : ".png"
post["w"] = 1200
post["h"] = 900
post["tn_w"] = 250
post["tn_h"] = 188
post["fsize"] = 100_000
seedMedia(timestamp: timestamp, index: number - id)
+ if isVideo { seedVideo(timestamp: timestamp) }
}
if arguments.contains("--ui-quote-fixture"), (103...105).contains(number) {
post["com"] = ">>\(number - 1)
Quoted reply \(number)"
@@ -69,6 +71,31 @@
ImageCache.default.store(image, forKey: "https://i.4cdn.org/biz/\(timestamp)s.jpg", toDisk: false)
}
+ /// Two seconds of silent teal WebM; avoids network-dependent video gallery tests.
+ private static func seedVideo(timestamp: Int) {
+ let encoded = """
+ GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAJ+EU2bdLpNu4tTq4QVSalmU6yBoU27i1OrhBZU
+ rmtTrIHWTbuMU6uEElTDZ1OsggEyTbuMU6uEHFO7a1OsggJo7AEAAAAAAABZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmsCrXsYMPQkBNgIxM
+ YXZmNjMuMS4xMDFXQYxMYXZmNjMuMS4xMDFEiYhAn0AAAAAAABZUrmvXrgEAAAAAAABO14EBc8WI5n7UnqvrF9WcgQAitZyDdW5k
+ iIEAhoVWX1ZQOIOBASPjg4QdzWUA4JCwgaC6gXiagQJVsIRVuYEBVe6BAOwBAAAAAAAAAgAAElTDZ/pzc59jwIBnyJlFo4dFTkNP
+ REVSRIeMTGF2ZjYzLjEuMTAxc3PVY8CLY8WI5n7UnqvrF9VnyKBFo4dFTkNPREVSRIeTTGF2YzYzLjEuMTAxIGxpYnZweGfIoUWj
+ iERVUkFUSU9ORIeTMDA6MDA6MDIuMDAwMDAwMDAwAB9DtnVAseeBAKPegQAAgBAHAJ0BKqAAeAAARwiFhYiFhIgCAgJ1qgP4AgaT
+ kRV2lQnFLSoTilpUJxS0qE4paVCcUtKhOKWlQnFLSoTilpSgAP77aJf/PTNeYP8nP/25H+3I/25H/7bCAKOYgQH0ABECAAEQEAAY
+ ABhYL/QACICBDLAAo5iBA+gAEQIAARAQABgAGFgv9AAIgIEMsACjmIEF3AARAgABEBAAGAAYWC/0AAiAgQywABxTu2uRu4+zgQC3
+ iveBAfGCAbHwgQM=
+ """
+ guard let data = Data(base64Encoded: encoded, options: .ignoreUnknownCharacters),
+ let url = URL(string: "https://i.4cdn.org/biz/\(timestamp).webm") else { return }
+ let file = CacheManager.shared.cacheURL(url)
+ do {
+ try FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try data.write(to: file, options: .atomic)
+ } catch {
+ assertionFailure("Unable to seed video fixture: \(error)")
+ }
+ }
+
static func catalog(board: String) throws -> Catalog? {
let arguments = ProcessInfo.processInfo.arguments
guard board == "biz" else { return nil }
diff --git a/swiftchan/Views/Media/Gallery/GalleryView.swift b/swiftchan/Views/Media/Gallery/GalleryView.swift
index 7c4bb39..44805a6 100644
--- a/swiftchan/Views/Media/Gallery/GalleryView.swift
+++ b/swiftchan/Views/Media/Gallery/GalleryView.swift
@@ -19,8 +19,7 @@ struct GalleryView: View {
@State private var selection: Int
@State private var canPage = true
- @State private var canShowPreview = true
- @State private var showPreview = false
+ @State private var showControls = false
@State private var canShowContextMenu = true
@State private var isSeeking = false
@State private var isZoomed = false
@@ -43,7 +42,6 @@ struct GalleryView: View {
.onDisappear {
restorePagerScrolling()
}
- .gesture(canShowPreview && showGalleryPreview ? showPreviewTap() : nil)
// Block the zoom transition's pan-to-dismiss while pinch-zoomed into
// media or scrubbing video, so those gestures keep priority.
.interactiveDismissDisabled(isZoomed || isSeeking)
@@ -51,9 +49,17 @@ struct GalleryView: View {
}
private var gallerySurface: some View {
- ZStack { galleryPager; galleryChrome }
+ ZStack {
+ galleryPager
+ .simultaneousGesture(galleryControlsTap())
+ galleryChrome
+ }
}
+ private var canShowControls: Bool { !isZoomed && !isSeeking }
+
+ private var showPreview: Bool { showControls && showGalleryPreview }
+
private var galleryPager: some View {
VerticalPagerView(
selection: $selection,
@@ -119,6 +125,7 @@ struct GalleryView: View {
VStack {
Spacer()
GalleryPreviewView(selection: $state.galleryIndex)
+ .accessibilityIdentifier("Gallery Preview")
.transition(.opacity)
.padding(.bottom, 60)
}
@@ -127,19 +134,17 @@ struct GalleryView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(alignment: .topLeading) { closeButton }
.overlay(alignment: .topTrailing) {
- if viewModel.media.indices.contains(selection) {
+ if showControls && canShowControls && viewModel.media.indices.contains(selection) {
ReverseImageSearchMenu(media: viewModel.media[selection], identifier: "Gallery Image Search")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white)
.padding(12)
.background(.black.opacity(0.65), in: Capsule())
.padding(16)
- .opacity(isZoomed || isSeeking ? 0 : 1)
- .allowsHitTesting(!isZoomed && !isSeeking)
}
}
.overlay(alignment: .bottomTrailing) {
- if viewModel.media.indices.contains(selection) {
+ if showControls && canShowControls && viewModel.media.indices.contains(selection) {
Text("\(selection + 1) of \(viewModel.media.count)")
.font(.caption.monospacedDigit().weight(.semibold))
.foregroundStyle(.white)
@@ -148,7 +153,6 @@ struct GalleryView: View {
.background(.black.opacity(0.65), in: Capsule())
.padding(16)
.padding(.bottom, positionBottomPadding)
- .opacity(isZoomed || isSeeking ? 0 : 1)
.allowsHitTesting(false)
.accessibilityLabel("Media \(selection + 1) of \(viewModel.media.count)")
.accessibilityIdentifier("Gallery Position")
@@ -188,18 +192,19 @@ struct GalleryView: View {
.onMediaChanged { zoomed in
isZoomed = zoomed
refreshPagingState()
- canShowPreview = !zoomed
canShowContextMenu = !zoomed
if zoomed {
- showPreview = false
+ showControls = false
}
onMediaChanged?(zoomed)
}
.onSeekChanged { seeking in
isSeeking = seeking
refreshPagingState()
- canShowPreview = !seeking
canShowContextMenu = !seeking
+ if seeking {
+ showControls = false
+ }
}
.mediaDownloadMenu(url: media.url, thumbnailURL: media.thumbnailUrl, canShowContextMenu: $canShowContextMenu)
.accessibilityIdentifier(
@@ -215,6 +220,7 @@ struct GalleryView: View {
onPageDragChanged?(.zero)
canShowContextMenu = true
+ showControls = false
isZoomed = false
isSeeking = false
refreshPagingState()
@@ -240,11 +246,12 @@ struct GalleryView: View {
viewModel.prefetch(currentIndex: index)
}
- func showPreviewTap() -> some Gesture {
+ private func galleryControlsTap() -> some Gesture {
TapGesture()
.onEnded {
+ guard canShowControls else { return }
withAnimation(.linear(duration: 0.2)) {
- showPreview.toggle()
+ showControls.toggle()
}
}
}
diff --git a/swiftchanUITests/BrowsingUITests.swift b/swiftchanUITests/BrowsingUITests.swift
index 87c5b31..940ef2d 100644
--- a/swiftchanUITests/BrowsingUITests.swift
+++ b/swiftchanUITests/BrowsingUITests.swift
@@ -284,7 +284,7 @@ final class BrowsingUITests: XCTestCase {
}
func testImageSearchIsAvailableFromPostsAndPagedGalleryMedia() {
- let app = launchThreadFixture(extraArguments: ["--ui-media-fixture"])
+ let app = launchThreadFixture(extraArguments: ["--ui-media-fixture", "-showGalleryPreview", "NO"])
openFixtureThread(in: app)
let search = app.buttons["Image Search Post 100"]
XCTAssertTrue(search.isHittable)
@@ -296,14 +296,24 @@ final class BrowsingUITests: XCTestCase {
app.tapThumbnailMedia(0)
XCTAssertTrue(app.buttons["Close gallery"].waitForExistence(timeout: 5))
let position = app.staticTexts["Gallery Position"]
+ assertGalleryControlsHidden(in: app)
+ attachScreenshot("Gallery Opens Without Search Or Counter")
+ app.galleryMediaImage(0).tap()
+ XCTAssertTrue(position.waitForExistence(timeout: 5))
+ XCTAssertTrue(app.buttons["Gallery Image Search"].isHittable)
XCTAssertEqual(position.label, "Media 1 of 3")
- attachScreenshot("Gallery Search Button And Position")
+ app.galleryMediaImage(0).tap()
+ assertGalleryControlsHidden(in: app)
+ app.galleryMediaImage(0).tap()
app.buttons["Gallery Image Search"].tap()
assertImageSearchProviders(in: app)
- attachScreenshot("Gallery Image Search")
position.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
app.galleryMediaImage(0).swipeUp()
XCTAssertTrue(app.galleryMediaImage(1).waitForExistence(timeout: 5))
+ assertGalleryControlsHidden(in: app)
+ attachScreenshot("Next Gallery Page Has No Search Or Counter")
+ app.galleryMediaImage(1).tap()
+ XCTAssertTrue(position.waitForExistence(timeout: 5))
let nextPosition = NSPredicate(format: "label == %@", "Media 2 of 3")
XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: nextPosition, object: position)], timeout: 5), .completed)
app.buttons["Gallery Image Search"].tap()
@@ -328,6 +338,88 @@ final class BrowsingUITests: XCTestCase {
assertImageSearchProviders(in: app)
}
+ func testPrepareGalleryCaptureOrientation() throws {
+ let orientation = ProcessInfo.processInfo.environment["GALLERY_CAPTURE_ORIENTATION"]
+ try XCTSkipUnless(orientation == "portrait" || orientation == "landscapeLeft", "Native gallery capture preparation only.")
+ prepareGalleryCaptureOrientation()
+ let app = launchThreadFixture(extraArguments: ["--ui-media-fixture"])
+ assertGalleryCaptureOrientation(in: app)
+ print("GALLERY_ORIENTATION_READY")
+ }
+
+ func testGalleryTapControlsPreservePreviewZoomAndLongPressActions() {
+ prepareGalleryCaptureOrientation()
+ let app = launchThreadFixture(extraArguments: ["--ui-media-fixture", "-showGalleryPreview", "YES"])
+ assertGalleryCaptureOrientation(in: app)
+ openFixtureThread(in: app)
+ app.tapThumbnailMedia(0)
+ let close = app.buttons["Close gallery"]
+ let search = app.buttons["Gallery Image Search"]
+ let position = app.staticTexts["Gallery Position"]
+ let preview = app.descendants(matching: .any)["Gallery Preview"].firstMatch
+ XCTAssertTrue(close.waitForExistence(timeout: 5))
+ assertGalleryControlsHidden(in: app)
+ XCTAssertFalse(preview.exists)
+ captureCatalogNavigation(app, "gallery_01_clean")
+ app.galleryMediaImage(0).tap()
+ XCTAssertTrue(search.waitForExistence(timeout: 5))
+ XCTAssertTrue(preview.exists)
+ XCTAssertEqual(position.label, "Media 1 of 3")
+ captureCatalogNavigation(app, "gallery_02_controls_revealed")
+ app.galleryMediaImage(0).tap()
+ assertGalleryControlsHidden(in: app)
+ XCTAssertFalse(preview.exists)
+ captureCatalogNavigation(app, "gallery_03_controls_hidden")
+ let originalWidth = app.galleryMediaImage(0).frame.width
+ app.galleryMediaImage(0).doubleTap()
+ XCTAssertGreaterThan(app.galleryMediaImage(0).frame.width, originalWidth * 1.5)
+ assertGalleryControlsHidden(in: app)
+ Thread.sleep(forTimeInterval: 0.6)
+ app.galleryMediaImage(0).doubleTap()
+ let unzoomed = NSPredicate { _, _ in abs(app.galleryMediaImage(0).frame.width - originalWidth) < 1 }
+ XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: unzoomed, object: nil)], timeout: 5), .completed)
+ app.galleryMediaImage(0).press(forDuration: 1)
+ XCTAssertTrue(app.buttons["Reverse Image Search"].waitForExistence(timeout: 5))
+ XCTAssertTrue(app.buttons["Open Original"].exists)
+ captureCatalogNavigation(app, "gallery_04_long_press_actions")
+ close.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
+ app.galleryMediaImage(0).swipeUp()
+ XCTAssertTrue(app.galleryMediaImage(1).waitForExistence(timeout: 5))
+ assertGalleryControlsHidden(in: app)
+ captureCatalogNavigation(app, "gallery_05_next_page_clean")
+ close.tap()
+ app.tapThumbnailMedia(1)
+ XCTAssertTrue(close.waitForExistence(timeout: 5))
+ assertGalleryControlsHidden(in: app)
+ close.tap()
+ }
+
+ func testVideoGalleryHidesThumbnailSearchUntilTapped() {
+ prepareGalleryCaptureOrientation()
+ let app = launchThreadFixture(extraArguments: ["--ui-media-fixture", "--ui-video-media-fixture", "-showGalleryPreview", "NO"])
+ assertGalleryCaptureOrientation(in: app)
+ openFixtureThread(in: app)
+ app.tapThumbnailMedia(2)
+ let close = app.buttons["Close gallery"]
+ XCTAssertTrue(close.waitForExistence(timeout: 5))
+ assertGalleryControlsHidden(in: app)
+ captureCatalogNavigation(app, "gallery_video_01_clean")
+ let media = app.descendants(matching: .any)["2 Gallery Media Image"].firstMatch
+ XCTAssertTrue(media.waitForExistence(timeout: 5))
+ media.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.4)).tap()
+ let search = app.buttons["Gallery Image Search"]
+ XCTAssertTrue(search.waitForExistence(timeout: 5))
+ XCTAssertEqual(search.label, "Search Thumbnail")
+ XCTAssertEqual(app.staticTexts["Gallery Position"].label, "Media 3 of 3")
+ XCTAssertTrue(app.sliders.firstMatch.exists)
+ captureCatalogNavigation(app, "gallery_video_02_controls_revealed")
+ media.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.4)).tap()
+ assertGalleryControlsHidden(in: app)
+ captureCatalogNavigation(app, "gallery_video_03_controls_hidden")
+ close.tap()
+ XCTAssertFalse(close.exists)
+ }
+
func testCopyQuoteUsesTheSelectedPostAndKeepsReadingPosition() {
let app = launchThreadFixture()
openFixtureThread(in: app, anchor: "#p105")
@@ -357,6 +449,29 @@ final class BrowsingUITests: XCTestCase {
attachScreenshot("Copied Quote Pasted Into Reply Draft")
}
+ private func assertGalleryCaptureOrientation(in app: XCUIApplication) {
+ let orientation = ProcessInfo.processInfo.environment["GALLERY_CAPTURE_ORIENTATION"]
+ guard orientation == "portrait" || orientation == "landscapeLeft" else { return }
+ let frame = app.windows.firstMatch.frame
+ XCTAssertGreaterThan(frame.width, 0)
+ XCTAssertGreaterThan(frame.height, 0)
+ XCTAssertEqual(frame.height > frame.width, orientation == "portrait")
+ print("GALLERY_CAPTURE_WINDOW: \(frame)")
+ }
+
+ private func prepareGalleryCaptureOrientation() {
+ switch ProcessInfo.processInfo.environment["GALLERY_CAPTURE_ORIENTATION"] {
+ case "portrait": XCUIDevice.shared.orientation = .portrait
+ case "landscapeLeft": XCUIDevice.shared.orientation = .landscapeLeft
+ default: break
+ }
+ }
+
+ private func assertGalleryControlsHidden(in app: XCUIApplication) {
+ XCTAssertFalse(app.staticTexts["Gallery Position"].exists)
+ XCTAssertFalse(app.buttons["Gallery Image Search"].exists)
+ }
+
private func assertImageSearchProviders(in app: XCUIApplication) {
for provider in ["Google Lens", "Yandex", "TinEye", "SauceNAO", "IQDB", "trace.moe"] {
XCTAssertTrue(app.buttons["Image Search \(provider)"].isHittable)
@@ -498,7 +613,8 @@ final class BrowsingUITests: XCTestCase {
app.buttons["Export Favorites"].tap()
// Files exposes different export controls at Browse and within a folder.
// Require the actual save picker and verify its filename when exposed.
- XCTAssertTrue(app.otherElements["Browse View (Picker)"].firstMatch.waitForExistence(timeout: 20))
+ // A cold hosted simulator can take over 30 seconds to initialize Files.
+ XCTAssertTrue(app.otherElements["Browse View (Picker)"].firstMatch.waitForExistence(timeout: 60))
let filename = app.textFields["DOCPicker.filenameTextField"]
let ready = NSPredicate { _, _ in filename.exists || app.buttons["Save"].exists }
XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: ready, object: nil)], timeout: 20), .completed)