Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement fetching posts/pages list in PostRepository #21814

Merged

Conversation

crazytonyli
Copy link
Contributor

@crazytonyli crazytonyli commented Oct 18, 2023

Description

This new implementation is a replacement of the syncing post function in PostService. However, it's not a direct translation.

Issues

The existing syncing post function has a few issues.

Issue 1: The syncing post function actually fetches all pages in the site, regardless what the function argument requests.

Here are two instances of the issue: #21813 and #21812.

The same scenarios will appear in other place that call the syncing post function. i.e. Site Settings, Menu. And, there is no way to disable this behaviour currently, because it's coded as an internal implementation detail.

For reference, probably not really an useful information, but #10264 is the PR that implemented this behaviour.

Issue 2: It's not clear when to use the purgesLocalSync argument.

When this argument is true, only the returned posts are kept in the local database and all other posts will be deleted.

A typical use case is posts/pages list. The list is implemented as displaying posts that's in Core Data database. The list changes when fetching posts API requests come back and new posts are added or updated in the database. As user scrolls to bottom of the list, the app sends new requests to load more posts. The purgesLocalSync value depending on different scenarios:

  1. purgesLocalSync = true when loading the first page. This can happen when user enters the screen, or pulls to refresh. In this scenario, we want to delete other posts in the database, so that only the first page is displayed, and other posts can be re-fetched when user continues to scroll through the list.
  2. purgesLocalSync = false when loading the following pages. This happens when user scrolls to the bottom of the list. In this scenario, we want to keep all loaded posts in the database so that all can be displayed on screen.

A definitely incorrect usage is #21754. The syncing post call should not set purgeLocalSync to true, because there is no need to only keep the three load pages and delete all others.

New implementation

The new implementation in PostRepository addresses the above two issues.

To address issue 2, I have created two funtions for different scenarios: paginate for loading paginated list (Posts List and Pages List) and search for finding pages without thinking about the state of the database.

For issue 1, the new implementation removes the ability to loading all pages. This of course is a reverse of the original PR #10264. But I can add a dedicated function loadAll later to be used in Pages List. And, there should be less chance that this loadAll function getting used in places where don't need to load all posts or pages.

Tests

I will add some unit tests in a follow up PR, because I want to spend dedicated time on writing some useful tests.

Update: I have added unit tests to this PR.

Regression Notes

  1. Potential unintended areas of impact
    None.

  2. What I did to test those areas of impact (or what existing automated tests I relied on)
    None.

  3. What automated tests I added (or what prevented me from doing so)
    See the "Tests" section.

PR submission checklist:

  • I have completed the Regression Notes.
  • I have considered adding unit tests for my changes.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

UI Changes testing checklist: N/A

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Oct 18, 2023

WordPress Alpha📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
App NameWordPress Alpha WordPress Alpha
ConfigurationRelease-Alpha
Build Numberpr21814-4dac5b7
Version23.4
Bundle IDorg.wordpress.alpha
Commit4dac5b7
App Center BuildWPiOS - One-Offs #7461
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Oct 18, 2023

Jetpack Alpha📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
App NameJetpack Alpha Jetpack Alpha
ConfigurationRelease-Alpha
Build Numberpr21814-4dac5b7
Version23.4
Bundle IDcom.jetpack.alpha
Commit4dac5b7
App Center Buildjetpack-installable-builds #6490
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@kean
Copy link
Contributor

kean commented Oct 18, 2023

@crazytonyli please keep in mind that the posts/pages list and search are re-worked almost entirely in #21789 and #21744. I would suggest targeting the task/ui-modernization-posts-and-pages feature branch for your changes too (went with a feature branch because the changes are localized to a handful of classes and it would've required too much effort to try and feature-flag it).

@crazytonyli
Copy link
Contributor Author

@kean Thanks for letting me know. I'll checkout the feature branch.

After a quick look at #21744, it appears there is no changes to how the PostService.syncPosts function is called. So, I think my change here would be simply additions on top of your UI modernization changes. And the new PostRepositor functions here can be used to replace all the PostService.syncPosts calls.

@crazytonyli crazytonyli marked this pull request as draft October 18, 2023 20:54
@crazytonyli crazytonyli changed the base branch from trunk to task/ui-modernization-posts-and-pages October 18, 2023 22:04
@crazytonyli crazytonyli force-pushed the tonyli-simplify-post-search branch from a3c12d9 to c9c3473 Compare October 18, 2023 22:04
@crazytonyli crazytonyli force-pushed the tonyli-simplify-post-search branch from c9c3473 to fa641ec Compare October 18, 2023 22:05
@crazytonyli crazytonyli marked this pull request as ready for review October 18, 2023 22:08
@crazytonyli crazytonyli requested a review from kean October 18, 2023 22:08
Comment on lines +252 to +254
func statuses() -> [String]? {
options.statuses
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered making this and the other computed vars?

I don't know if there's a recommendation somewhere. It might just be my personal preference, but I find that this kind of getters read better as properties than functions. I guess methods give me the idea that something is happening under the hood, whereas properties feel more like reading information.


Update: Never mind... This conforms to the PostServiceRemoteOptions protocol (a protocol that should be a struct by now...) and therefore they need to be func:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Objective-C...

Comment on lines +388 to +389
// There is an assertion above to ensure the app doesn't fall into this case.
return []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered fatalError, given this should be an unreachable code path?

Or, is return [] a safeguard for the production code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just don't want to duplicate the assertion code and its messaging. 😄

Comment on lines 421 to 426
return updatedPosts.compactMap {
guard let post = $0 as? P else {
fatalError("Expecting a \(postType) as \(type), but got \($0)")
}
return TaggedManagedObjectID(post)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was surprised by the compactMap given TaggedManagedObjectID.init is not of the Optional kind.

Then I thought it might be because of the PostHelper merge... comes from the Objective-C layer, and doesn't have nullability annotation. But I saw it has NS_ASSUME_NONNULL_BEGIN and _END.

That's all to say, maybe map would do here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Updated in 4dac5b7

Comment on lines +9 to +23
var repository: PostRepository!
var blogID: TaggedManagedObjectID<Blog>!

override func setUp() async throws {
repository = PostRepository(coreDataStack: contextManager)

let loggedIn = try await signIn()
blogID = try await contextManager.performAndSave {
let blog = try BlogBuilder($0)
.with(dotComID: 42)
.withAccount(id: loggedIn)
.build()
return TaggedManagedObjectID(blog)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you feel about making these into methods that each test can call as part of it's arrange step?

We'd have to code a bit more, but it would prevent the test case from holding repository and blogID in memory after it runs. (I haven't verified the behavior in Xcode 15, but it's a community documented quirk).

To be specific:

// We could alternatively read coreDataStack from contextManager in the implementation
// to keep the method as requiring no arguments.
func makeRepository(coreDataStack: CoreDataStack) -> PostRepository {
    PostRepository(coreDataStack: coreDataStack)
}

func makeBlogID(coreDataStack: CoreDataStack) async throws -> TaggedManagedObjectID<Blog> {
     // ...
}

func testPagination() async throws {
    let repository = makeRepository(...)
    let blogID = try await makeBlogID(...)
    // Given there are 15 published posts on the site
    try await preparePostsList(type: "post", total: 15)

   // ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware of this weird XCTest behaviour. But, I don't think we should avoid using instance variables just because this weird behaviour exists? Of course, making everything transient and deallocated after each test run would reduce chances of potential issues. But that feels like a lot afford, in terms of coding, especially if we know those instance variables—even though being "leaked"–should not introduce problems.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that feels like a lot afford, in terms of coding

Is it extra effort, though?

Most of the coding has to be done regardless. One version has it in make... functions, the other in the setup.

There's an additional advantage: each test method is self contained and sets up all and only what it needs.

I've seen test (not sure if in this codebase, but I can look it up) where steps taken in the setUp had to be partially undone in individual tests because of different state expectations. Of course, one could argue that if a test has to do that much setup, maybe the problem is in the software design...

Anyways, I think this is a conversation broader than the scope of this PR 😄 Thank you for sharing your point of view. I'll P2 at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it extra effort, though?

Maybe not too bad in this particular test case where there is only two properties. But for other test cases that have more properties, we'll have to repeat the instantiation statements in all test functions, which feels like a bit too much...

My point is this setting up test environment code belongs to the setUp function, and it'd be much nicer to keep them there. If we have to make a convention, an alternative to moving those instantiation code out of setUp and into the test functions is, setting those properties to nil in tearDown. We kind of get benefits from both world: setting up test environment code is consolidated in the setUp function and they won't be kept in memory forever due to how XCTest runs tests.


}

// MARK: - Tests that ensure the `preparePostsList` works as expected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar nitpick

Suggested change
// MARK: - Tests that ensure the `preparePostsList` works as expected
// MARK: - Tests that ensures the `preparePostsList` works as expected
Suggested change
// MARK: - Tests that ensure the `preparePostsList` works as expected
// MARK: - Tests to ensure the `preparePostsList` works as expected


}

private extension PostRepositoryPostsListTests {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Reading the test top to bottom, I was surprised by the Stub in the tests names above. I think if I'd seen the method definition before the tests, the logic would have been clearer.

Alternatively, the method could live in a dedicated test helper object, with dedicated test file. But I wouldn't recommend going to that extent unless we were to reuse it elsewhere.

XCTAssertEqual(total, 15)
}

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered adding tests to ensure the orderBy and descending parameters are respected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the preparePostsList API doc, these two parameters are ignored intentionally. I didn't explain there why though. It's because I don't think implementing sorting would mean much to the test case. Asserting the returned posts are in expected order doesn't test any app code. The app code doesn't really care about the order: it blindly trusts the API would return posts in the correct order.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right right. I got confused, my bad.

I feel like if the parameters are given, we should somehow test they are passed along correctly... 🤔

Then again, as far as business logic goes, that's quite straightforward. The cost of debugging if a mistake is made should be low enough.

@crazytonyli
Copy link
Contributor Author

There is one UI test failure in this PR, but I doubt it's caused by this PR's changes: None of this PR's new code is executed by the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants