~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
164 lines · 5.3 KB Raw History
@testable import Grain
import SwiftUI
import XCTest

/// The screens presented as sheets or pushes from the feed and story viewer.
/// None of them was reachable from the suite, and between them they are a large
/// share of the app's view code.
@MainActor
final class SheetRenderTests: GrainTestCase {
    private var account: TestAccount!

    /// Async overrides: the synchronous `setUp`/`tearDown` are nonisolated, so
    /// touching main-actor state from them warns.
    override func setUp() async throws {
        try await super.setUp()
        account = TestAccount()
    }

    override func tearDown() async throws {
        MockURLProtocol.handler = nil
        account.restore()
        try await super.tearDown()
    }

    // MARK: - Sign in

    func testRendersLogin() {
        let env = TestEnvironment(authenticated: false)
        ViewRender.render(LoginView().withTestEnvironment(env))
    }

    // MARK: - Camera feed

    func testRendersCameraFeed() {
        MockURLProtocol.respondWithJSON(Fixtures.feed)
        let env = TestEnvironment()
        ViewRender.render(
            CameraFeedView(client: env.client, camera: "Fujifilm X100V").withTestEnvironment(env)
        )
    }

    func testRendersCameraFeedWhenEmpty() {
        MockURLProtocol.respondWithJSON(#"{"items": [], "cursor": null}"#)
        let env = TestEnvironment()
        ViewRender.render(
            CameraFeedView(client: env.client, camera: "Leica M6").withTestEnvironment(env)
        )
    }

    // MARK: - Story creation

    func testRendersStoryCreate() {
        MockURLProtocol.respondWithJSON("{}")
        let env = TestEnvironment()
        ViewRender.render(
            StoryCreateView(client: env.client, onCreated: {}).withTestEnvironment(env)
        )
    }

    // MARK: - Gallery comment sheet

    /// The sheet owns its own view model and loads the thread on appear, so a
    /// render covers both the loading and the loaded branches.
    func testRendersGalleryCommentSheet() {
        MockURLProtocol.handler = { request in
            let body = request.url!.path.contains("getGallery")
                ? Fixtures.galleryResponse
                : Fixtures.commentThread
            let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
            return (Data(body.utf8), response)
        }
        let env = TestEnvironment()

        ViewRender.render(
            CommentSheetView(
                client: env.client,
                galleryUri: "at://did:plc:test/social.grain.gallery/1",
                onProfileTap: { _ in },
                onHashtagTap: { _ in },
                onCommentCountChanged: { _ in }
            )
            .withTestEnvironment(env),
            settle: 0.2
        )
    }

    func testRendersGalleryCommentSheetWhenTheThreadFailsToLoad() {
        MockURLProtocol.respondWithError(statusCode: 500)
        let env = TestEnvironment()

        ViewRender.render(
            CommentSheetView(
                client: env.client,
                galleryUri: "at://did:plc:test/social.grain.gallery/1"
            )
            .withTestEnvironment(env)
        )
    }

    // MARK: - Story comment sheet

    func testRendersStoryCommentSheet() {
        MockURLProtocol.respondWithJSON(Fixtures.commentThread)
        let env = TestEnvironment()
        let viewModel = StoryCommentsViewModel(client: env.client)

        ViewRender.render(
            StoryCommentSheet(
                viewModel: viewModel,
                storyUri: "at://did:plc:test/social.grain.story/1",
                client: env.client,
                onProfileTap: { _ in },
                onDismiss: {}
            )
            .withTestEnvironment(env),
            settle: 0.2
        )
    }

    /// Opening the sheet from the comment button focuses the input, which puts
    /// the keyboard accessory and the mention strip on screen.
    func testRendersStoryCommentSheetWithTheInputFocused() {
        MockURLProtocol.respondWithJSON(#"{"comments": [], "cursor": null}"#)
        let env = TestEnvironment()

        ViewRender.render(
            StoryCommentSheet(
                viewModel: StoryCommentsViewModel(client: env.client),
                storyUri: "at://did:plc:test/social.grain.story/1",
                client: env.client,
                focusInput: true
            )
            .withTestEnvironment(env),
            settle: 0.2
        )
    }

    // MARK: - Comment presenter

    /// Without `configure` the presenter has no environment to hand the sheet,
    /// so it must decline rather than present a sheet with no auth in it.
    func testTheCommentPresenterDeclinesToOpenBeforeItIsConfigured() {
        let env = TestEnvironment()
        let presenter = StoryCommentPresenter()

        presenter.open(
            storyUri: "at://did:plc:test/social.grain.story/1",
            focusInput: false,
            commentsViewModel: StoryCommentsViewModel(client: env.client),
            client: env.client
        )

        XCTAssertNil(presenter.presentedStoryUri)
    }

    /// Closing something that was never opened has to be harmless — the story
    /// viewer calls it on teardown regardless.
    func testClosingAnUnopenedCommentPresenterIsHarmless() {
        let presenter = StoryCommentPresenter()
        presenter.close()

        XCTAssertNil(presenter.presentedStoryUri)
    }
}