[component][typeahead-suggestion] Add network client for typeahead-search component #72
Conversation
In most browsers we can use window.fetch. In others, like IE 11, this is not available. To keep this simple for now reject the promise if native fetch is not available. Added jest-fetch-mock for testing the "fetch is available" branch.
A convenience method to serialize a JS object into a query string, so it can be appended to the URL (after '?').
This network client provides a fetchByTitle() function, to be used for typeahead search suggestions. Not providing accept-language header at this time since it does not seem to make a difference.
By default the requests are mocked, so no network is used. If the environment variable TEST_LIVE_REQUESTS is set to a truthy value, the mocks are disabled and requests are made against a live server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! 👍 A couple questions inline.
// A wrapper for native fetch() in browsers. | ||
// Currently this rejects the returned promise if window.fetch is not available in the browser. | ||
// The plan is to add a fallback so we can support older browsers in the future. | ||
export function fetch( resource: string, init?: RequestInit ): Promise<Response> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you noted, we know we'll need to support IE11. How best to go about it? These are some of the concerns that come to mind:
- We want to be able to easily test either in implementation.
- This client is currently touching the Window global. State and especially globals can be problematic but is this ok?
- IE11 also doesn't support Promises.
- The library will be loaded and configured once but may be depended upon by multiple codebases, not just Vector.
Did you consider passing window.fetch
/ $.ajax
/ custom Fetch implementation as a dependency of the SearchClient, along with query and host, instead of creating this utility function? Passing fetch would allow you to avoid relying on the window
global and allow to test any implementation you wanted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Support for IE11 is tough here without adding to the bundle size. Not sure how much bloat to the bundle size is acceptable here.
Passing in a fetch implementation seems problematic since window.fetch
and $.ajax
behave slightly differently. We would have to wrap the $.ajax
call if we wanted to make it behave like fetch.
IE11 not supporting Promises throws in another wrench. Makes me wonder if I should drop fetch for now and go with callbacks and XHR. (So far I only see await used in a few tests, beside what this PR adds.)
I wonder if it's worth doing a prototype using a ponyfill for Promises and fetch at all, to see how much it adds to the bundle size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the jQuery promises are actually ES6 compatible? I think it'd be ok if Vector unconditionally passed $.ajax in if that simplifies things. We just shouldn't assume the fetch implementation in the library.
https://jquery.com/upgrade-guide/3.0/#breaking-change-and-feature-jquery-deferred-is-now-promises-a-compatible
https://www.mediawiki.org/wiki/JQuery
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, fetch is not a requirement we can demand due to the support requirements. Our options are:
- Ask the client to pass in a fetch implementation (likely to be jQuery). Unofficial note on some differences.
- Pony/polyfill fetch and Promise. This will increase our bandwidth usage and may still behave differently from native fetch. For instance, see these caveats for the Preact guy / MDN's implementation.
- Use XHR. Callbacks but on the plus side we cancel requests.
- Something else?
@speedandfunction-anribolon, thoughts on using an XMLHttpRequest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@niedzielski @berndsi I wouldn't use XMLHttpRequest in spite of it's a bulletproof solution. I'd better stick with polyfills. Maybe we can deliver separate bundles for IE including polyfills for fetch and promises and "polifill-less" bundle for the rest of browsers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't used this but es6-promise is about 2.4 kB and unfetch is about .5 kB. Thoughts? /cc @speedandfunction-anribolon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was also considering the 'core-js/modules/es6.promise'
part of core-js, as described in Loading Polyfills Only When Needed. The alternative for unfetch was whatwg-fetch but that is quite a bit larger at 3.1kB.
Before we drill down more into various options I thought there was a more general concern about having an extra bundle for older browser, was there not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this discussion to Slack so Web can weigh in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@niedzielski Should we merge the fetch implementation, then create a separate PR for XMLHttpRequest? That way we could revert the latter commit to get back to a pure fetch implementation. If this is the case, are there any other open issues with this PR?
Side note: there is now a new Phab task to add a fetch polyfill to mediawiki core.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merging with the expectation that we will use XHR or polyfill ASAP.
jestFetchMock.enableFetchMocks(); | ||
const mockedRequests = !process.env.TEST_LIVE_REQUESTS; | ||
|
||
describe( 'fetch() using window.fetch', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we need these tests? I'm unsure. When it's not mocked, we test the actual Fetch implementation but that's going to be either window.fetch() or $.ajax(), neither of which is ours.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally I had this hitting example.org and had them as more general fetch calls. Now that I switched to something very similar to what the search client does the 200 response tests do seem redundant. I'd probably still prefer to keep the 404 test. I was actually considering adding some more tests for network issues since the jQuery version does behave quite differently. (It throws exceptions, uses different fields than fetch.)
src/components/typeahead-search/http/restApiSearchClient.test.ts
Outdated
Show resolved
Hide resolved
A repeat of `title` doesn't seem useful to me.
Also undo in afterAll(). Plus a formatting fix.
beforeEach( () => { | ||
fetchMock.resetMocks(); | ||
if ( !mockedRequests ) { | ||
fetchMock.disableMocks(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this appear in both beforeEach and afterAll?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for the one in afterAll()
was to undo what was done in beforeAll()
.
Not exactly sure what's going on here. The comment in the d.ts file says
// alias of fetchMock.disableMocks() for ease of ES6 import syntax to not clash with other libraries
When I try using just fetchMock.disableMocks()
instead of jestFetchMock.disableFetchMocks()
that works but we lose the symmetry of beforeAll and afterAll. When also replacing the enableMocks
call it forces me to change the jestFetch mock import. I tried just import 'jest-fetch-mock'
but that fails the tests: ReferenceError: fetchMock is not defined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't used jest-fetch-mock (or at least don't remember having used it). I'm looking at the docks and wondering if you considered using mockOnce()
or mockOnceIf()
. It seems like those might be cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs of mockOnce()
seems to imply that it always mocks:
but guarantees the next call to fetch will be mocked even if the default behavior of fetchMock is to use the real implementation
I think it would be nice to have the option to run these tests against a real server to keep these grounded in reality, and not just make up some responses that are unlikely to happen in the real world.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see but wouldn't this move to just before the call to fetchByTitle()
in the tests themselves? For example, fetch.doMockOnceIf( mockedRequests )
.
This isn't a big deal on it's own but I wanted to encourage you to take the time to write the best network tests here as possible since others contributors are likely to copy your approach. At the other extreme, I remember in the Android app we had trouble with tests depending on other tests running in a certain order or being unable to run in parallel because of configuration changes to globals. I think the before/after parity you've given here will help us avoid those issues.
Then again, maybe all this breaks if change the underlying implementation from fetch to XHR 😭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@niedzielski I'm not sure I understand the first paragraph of your most recent comment. fetchMock.mockResponse()
is right before the call to fetchByTitle()
. Are you asking to just replace it with fetch.doMockOnceIf()
or something else?
Yes, if we change to implementation away from fetch this all seems mood.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed this to use mockOnce now.
src/components/typeahead-search/http/restApiSearchClient.test.ts
Outdated
Show resolved
Hide resolved
// A wrapper for native fetch() in browsers. | ||
// Currently this rejects the returned promise if window.fetch is not available in the browser. | ||
// The plan is to add a fallback so we can support older browsers in the future. | ||
export function fetch( resource: string, init?: RequestInit ): Promise<Response> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, fetch is not a requirement we can demand due to the support requirements. Our options are:
- Ask the client to pass in a fetch implementation (likely to be jQuery). Unofficial note on some differences.
- Pony/polyfill fetch and Promise. This will increase our bandwidth usage and may still behave differently from native fetch. For instance, see these caveats for the Preact guy / MDN's implementation.
- Use XHR. Callbacks but on the plus side we cancel requests.
- Something else?
@speedandfunction-anribolon, thoughts on using an XMLHttpRequest?
Doing that I noticed that the null values need to be called out in the interface.
so it can be used by the UI for highlighting the search text.
src/components/typeahead-search/http/restApiSearchClient.test.ts
Outdated
Show resolved
Hide resolved
Titles don't start or end with spaces. So, remove them, at least for title search.
The REST API documentation says it could return null for width or height. Make the value undefined in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
src/components/typeahead-search/http/restApiSearchClient.test.ts
Outdated
Show resolved
Hide resolved
Clarify that the input object changes in one place where the thumbnail is null.
src/components/typeahead-search/http/restApiSearchClient.test.ts
Outdated
Show resolved
Hide resolved
src/components/typeahead-search/http/restApiSearchClient.test.ts
Outdated
Show resolved
Hide resolved
@@ -0,0 +1,73 @@ | |||
import { buildQueryString, fetch } from '../../../http/fetch'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@niedzielski Do you know why changing this to '@/http/fetch'
doesn't work? I'm getting
Cannot find module '@/http/fetch' from 'src/components/typeahead-search/http/restApiSearchClient.ts'
Require stack:
src/components/typeahead-search/http/restApiSearchClient.ts
src/components/typeahead-search/http/restApiSearchClient.test.ts
> 1 | import { buildQueryString, fetch } from '@/http/fetch';
| ^
2 | import { SearchClient, SearchResponse } from './SearchClient';
3 |
4 | // https://www.mediawiki.org/wiki/API:REST_API/Reference#Search_result_object
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:308:11)
at Object.<anonymous> (src/components/typeahead-search/http/restApiSearchClient.ts:1:1)
This network client provides fetchByTitle(), to be used for typeahead search suggestions.
Not providing accept-language header at this time since it does not seem to make a difference.
By default the requests are mocked, so no network is used.
If the environment variable TEST_LIVE_REQUESTS is set to a truthy value, the mocks are
disabled and requests are made against a live server.
Bug: T244287