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

Refactor waitFor* actions with resolveSelect. #9978

Merged
merged 21 commits into from
Jan 17, 2025

Conversation

techanvil
Copy link
Collaborator

@techanvil techanvil commented Jan 6, 2025

Summary

Addresses issue:

Relevant technical choices

PR Author Checklist

  • My code is tested and passes existing unit tests.
  • My code has an appropriate set of unit tests which all pass.
  • My code is backward-compatible with WordPress 5.2 and PHP 7.4.
  • My code follows the WordPress coding standards.
  • My code has proper inline documentation.
  • I have added a QA Brief on the issue linked above.
  • I have signed the Contributor License Agreement (see https://cla.developers.google.com/).

Do not alter or remove anything below. The following sections will be managed by moderators only.

Code Reviewer Checklist

  • Run the code.
  • Ensure the acceptance criteria are satisfied.
  • Reassess the implementation with the IB.
  • Ensure no unrelated changes are included.
  • Ensure CI checks pass.
  • Check Storybook where applicable.
  • Ensure there is a QA Brief.
  • Ensure there are no unexpected significant changes to file sizes.

Merge Reviewer Checklist

  • Ensure the PR has the correct target branch.
  • Double-check that the PR is okay to be merged.
  • Ensure the corresponding issue has a ZenHub release assigned.
  • Add a changelog message to the issue.

@techanvil techanvil force-pushed the enhancement/9008-remove-waitfor-actions branch from dfc9788 to 7cea708 Compare January 6, 2025 17:20
Copy link

github-actions bot commented Jan 6, 2025

Build files for 7b78c6f have been deleted.

Copy link

github-actions bot commented Jan 6, 2025

Size Change: -1.21 kB (-0.06%)

Total Size: 1.98 MB

Filename Size Change
./dist/assets/js/googlesitekit-ad-blocking-recovery-********************.js 54.2 kB +1 B (0%)
./dist/assets/js/googlesitekit-adminbar-********************.js 34.8 kB -2 B (-0.01%)
./dist/assets/js/googlesitekit-api-********************.js 10.1 kB +1 B (+0.01%)
./dist/assets/js/googlesitekit-components-gm2-********************.js 6.18 kB -1 B (-0.02%)
./dist/assets/js/googlesitekit-data-********************.js 2.37 kB +1 B (+0.04%)
./dist/assets/js/googlesitekit-datastore-forms-********************.js 8.94 kB +1 B (+0.01%)
./dist/assets/js/googlesitekit-datastore-location-********************.js 2.08 kB +1 B (+0.05%)
./dist/assets/js/googlesitekit-datastore-site-********************.js 20.2 kB -21 B (-0.1%)
./dist/assets/js/googlesitekit-datastore-ui-********************.js 10 kB +1 B (+0.01%)
./dist/assets/js/googlesitekit-datastore-user-********************.js 28.2 kB -6 B (-0.02%)
./dist/assets/js/googlesitekit-entity-dashboard-********************.js 82.6 kB +1 B (0%)
./dist/assets/js/googlesitekit-main-dashboard-********************.js 167 kB -4 B (0%)
./dist/assets/js/googlesitekit-metric-selection-********************.js 52 kB +2 B (0%)
./dist/assets/js/googlesitekit-modules-********************.js 22.3 kB -3 B (-0.01%)
./dist/assets/js/googlesitekit-modules-ads-********************.js 35.8 kB +11 B (+0.03%)
./dist/assets/js/googlesitekit-modules-adsense-********************.js 119 kB -106 B (-0.09%)
./dist/assets/js/googlesitekit-modules-analytics-4-********************.js 190 kB -964 B (-0.5%)
./dist/assets/js/googlesitekit-modules-pagespeed-insights-********************.js 22.6 kB -1 B (0%)
./dist/assets/js/googlesitekit-modules-reader-revenue-manager-********************.js 40.1 kB -2 B (0%)
./dist/assets/js/googlesitekit-modules-search-console-********************.js 69.2 kB +12 B (+0.02%)
./dist/assets/js/googlesitekit-modules-sign-in-with-google-********************.js 31.6 kB +3 B (+0.01%)
./dist/assets/js/googlesitekit-modules-tagmanager-********************.js 32.2 kB -136 B (-0.42%)
./dist/assets/js/googlesitekit-notifications-********************.js 37.9 kB -5 B (-0.01%)
./dist/assets/js/googlesitekit-settings-********************.js 127 kB +6 B (0%)
./dist/assets/js/googlesitekit-splash-********************.js 68.7 kB -4 B (-0.01%)
./dist/assets/js/googlesitekit-user-input-********************.js 43.9 kB -3 B (-0.01%)
./dist/assets/js/googlesitekit-vendor-********************.js 323 kB +2 B (0%)
./dist/assets/js/googlesitekit-wp-dashboard-********************.js 63.3 kB -4 B (-0.01%)
./dist/assets/js/runtime-********************.js 1.4 kB +6 B (+0.43%)
ℹ️ View Unchanged
Filename Size
./dist/assets/css/googlesitekit-admin-css-********************.min.css 62.2 kB
./dist/assets/css/googlesitekit-adminbar-css-********************.min.css 11.8 kB
./dist/assets/css/googlesitekit-authorize-application-css-********************.min.css 846 B
./dist/assets/css/googlesitekit-wp-dashboard-css-********************.min.css 8.48 kB
./dist/assets/js/33-********************.js 2.76 kB
./dist/assets/js/34-********************.js 2.25 kB
./dist/assets/js/35-********************.js 3.64 kB
./dist/assets/js/36-********************.js 936 B
./dist/assets/js/37-********************.js 893 B
./dist/assets/js/38-********************.js 1.61 kB
./dist/assets/js/39-********************.js 1.57 kB
./dist/assets/js/40-********************.js 1.61 kB
./dist/assets/js/41-********************.js 1.59 kB
./dist/assets/js/42-********************.js 1.83 kB
./dist/assets/js/43-********************.js 3.12 kB
./dist/assets/js/analytics-advanced-tracking-********************.js 901 B
./dist/assets/js/googlesitekit-activation-********************.js 24 kB
./dist/assets/js/googlesitekit-components-gm3-********************.js 10.1 kB
./dist/assets/js/googlesitekit-consent-mode-********************.js 25.6 kB
./dist/assets/js/googlesitekit-events-provider-contact-form-7-********************.js 646 B
./dist/assets/js/googlesitekit-events-provider-easy-digital-downloads-********************.js 624 B
./dist/assets/js/googlesitekit-events-provider-mailchimp-********************.js 630 B
./dist/assets/js/googlesitekit-events-provider-ninja-forms-********************.js 712 B
./dist/assets/js/googlesitekit-events-provider-optin-monster-********************.js 675 B
./dist/assets/js/googlesitekit-events-provider-popup-maker-********************.js 634 B
./dist/assets/js/googlesitekit-events-provider-woocommerce-********************.js 657 B
./dist/assets/js/googlesitekit-events-provider-wpforms-********************.js 633 B
./dist/assets/js/googlesitekit-i18n-********************.js 3.93 kB
./dist/assets/js/googlesitekit-polyfills-********************.js 377 B
./dist/assets/js/googlesitekit-reader-revenue-manager-block-editor-********************.js 477 B
./dist/assets/js/googlesitekit-widgets-********************.js 102 kB

compressed-size-action

@ankitrox
Copy link
Collaborator

ankitrox commented Jan 7, 2025

LGTM 💯

Moving this to MR.

Copy link
Collaborator

@aaemnnosttv aaemnnosttv left a comment

Choose a reason for hiding this comment

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

Thanks @techanvil – this looks solid overall. Looks like I missed the detail about AMP containers in the IB but it should be quick to address. A few other questions/suggestions below as well.

assets/js/googlesitekit/data/create-existing-tag-store.js Outdated Show resolved Hide resolved
Comment on lines 88 to 90
fetchMock.getOnce(
new RegExp(
'^/google-site-kit/v1/modules/analytics-4/data/account-summaries'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this intentionally defined after render? I don't think we could be sure it would work this way.

Copy link
Collaborator Author

@techanvil techanvil Jan 9, 2025

Choose a reason for hiding this comment

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

It is, yup. It was a bit of an odd one that took some digging to figure out what was going on.

I've added a clarifying comment, basically it's a side effect of adding the resolver for getPropertySummaries() which resolves getAccountSummaries().

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks @techanvil – is it important that the resolver finishes? It seems to me that it would be more appropriate to update the fetch mock for the same endpoint above to freeze fetch "manually" using fetchMock.get rather than once so that it would handle both requests. I think the context of the comment is useful, but even if we keep the same mock, we should define it before render. This should be possible since it supports defining multiple matchers/mocks as well as simple repetition. See https://www.wheresrhys.co.uk/fetch-mock/docs/legacy-api/Usage/cheatsheet#timing-and-repetition

Copy link
Collaborator Author

@techanvil techanvil Jan 16, 2025

Choose a reason for hiding this comment

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

It's not essential that the resolver finishes, and I'm happy to freeze both requests. I had initially done so, in fact, but decided to take this approach instead as I figured it spells out more clearly the precise flow of events.

However, happy to make the change and I have done so.

Although using fetchMock.get() to "manually" freeze the request works, I've taken the approach of modifying freezeFetch() to allow a specific number of requests to be frozen. This retains the semantic value of calling freezeFetch() while making the test setup more explicit.

assets/js/modules/tagmanager/datastore/containers.js Outdated Show resolved Hide resolved
Copy link
Collaborator

@aaemnnosttv aaemnnosttv left a comment

Choose a reason for hiding this comment

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

Thanks @techanvil ! This is almost there, just a few comments left to address.

Comment on lines 88 to 90
fetchMock.getOnce(
new RegExp(
'^/google-site-kit/v1/modules/analytics-4/data/account-summaries'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks @techanvil – is it important that the resolver finishes? It seems to me that it would be more appropriate to update the fetch mock for the same endpoint above to freeze fetch "manually" using fetchMock.get rather than once so that it would handle both requests. I think the context of the comment is useful, but even if we keep the same mock, we should define it before render. This should be possible since it supports defining multiple matchers/mocks as well as simple repetition. See https://www.wheresrhys.co.uk/fetch-mock/docs/legacy-api/Usage/cheatsheet#timing-and-repetition

Comment on lines 766 to 774
const summaries =
select( MODULES_ANALYTICS_4 ).getAccountSummaries( accountID );

if ( summaries === undefined ) {
yield commonActions.await(
resolveSelect( MODULES_ANALYTICS_4 ).getAccountSummaries(
accountID
)
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can and should simplify this to be unconditional since there's no downside to calling the selector here. This kind of guard is only really necessary around fetch actions, but the WP data resolver infra will only run once regardless of when this is called so it should be perfectly safe to do.

Also, there could be a situation where the underlying resolver is invalidated where, using the current logic here would prevent it from running when it should.

Copy link
Collaborator Author

@techanvil techanvil Jan 16, 2025

Choose a reason for hiding this comment

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

Thanks @aaemnnosttv, it does make sense to unconditionally call the resolver, and I've applied that change.

However, unless I've missed something, there are scenarios where a resolver can be called twice, as mentioned in my comment on the PropertyOrWebDataStreamNotAvailableError test in this PR.

/*
* We need to mock the `GET:account-summaries` request again here, as it will be called a
* second time while resolvers are run post-render. This is due to the initial endpoint
* response being frozen, meaning the `getAccountSummaries()` resolver is unable to resolve
* itself the first time around, and will be called again when `getAccountSummaries()` is
* re-selected by another resolver.
*
* In practice, this is not an issue because the `PropertyOrWebDataStreamNotAvailableError`
* component is not rendered until the account summaries are already loaded.
*/

This can be verified in a simple manner by isolating that test and adding a log line in the *getAccountSummaries() resolver, which will log twice for the test run.

diff --git a/assets/js/modules/analytics-4/components/settings/PropertyOrWebDataStreamNotAvailableError.test.js b/assets/js/modules/analytics-4/components/settings/PropertyOrWebDataStreamNotAvailableError.test.js
index 14be657ce4..26a2837a95 100644
--- a/assets/js/modules/analytics-4/components/settings/PropertyOrWebDataStreamNotAvailableError.test.js
+++ b/assets/js/modules/analytics-4/components/settings/PropertyOrWebDataStreamNotAvailableError.test.js
@@ -62,7 +62,7 @@ describe( 'PropertyOrWebDataStreamNotAvailableError', () => {
 			.setMeasurementID( measurementID );
 	} );
 
-	it( 'should not render when properties are not loaded yet', async () => {
+	it.only( 'should not render when properties are not loaded yet', async () => {
 		freezeFetch(
 			new RegExp(
 				'^/google-site-kit/v1/modules/analytics-4/data/account-summaries'
diff --git a/assets/js/modules/analytics-4/datastore/accounts.js b/assets/js/modules/analytics-4/datastore/accounts.js
index 473dd06b07..782be3271f 100644
--- a/assets/js/modules/analytics-4/datastore/accounts.js
+++ b/assets/js/modules/analytics-4/datastore/accounts.js
@@ -338,8 +338,14 @@ const baseReducer = createReducer( ( state, { type } ) => {
 	}
 } );
 
+// Avoid console.log in tests.
+const log = ( ...args ) =>
+	process.stdout.write( args.map( JSON.stringify ).join( ' ' ) + '\n' );
+
 const baseResolvers = {
 	*getAccountSummaries() {
+		log( 'getAccountSummaries resolver' );
+
 		const registry = yield commonActions.getRegistry();
 		let nextPageToken = '';
 		const summaries = registry

I don't think this means we need to change anything, just pointing it out as a bit of unexpected behaviour to be aware of.

Comment on lines 211 to 213
if ( ! isValidPropertyID( propertyID ) ) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about we move this guard to getWebDataStreams below? Then this function can resolve-select that here unconditionally similar to my comment above.

Copy link
Collaborator Author

@techanvil techanvil Jan 16, 2025

Choose a reason for hiding this comment

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

Sounds good - just to be clear though, did you mean to move it to the *getWebDataStreams() resolver? That's what I have done, but arguably it would be more consistent to move it to an invariant() check in the getWebDataStreams() selector.

I was a bit hesitant to add the invariant() call, because it could materially change the current execution path and user flow in some edge case out there, but I'm interested to get your thoughts on it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep, that's what I meant (the resolver). It just doesn't make sense to fetch for an invalid property ID 👍

assets/js/modules/tagmanager/datastore/containers.js Outdated Show resolved Hide resolved
Copy link
Collaborator

@aaemnnosttv aaemnnosttv left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks @techanvil 👍

Comment on lines +516 to +517
// An additional wait is required in order for all resolvers to finish.
await act( waitForDefaultTimeouts );
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be more appropriate to use a secondary waitForRegistry here instead of an arbitrary timeout. We can always come back to it if needed though.

e.g.

diff --git a/assets/js/modules/analytics-4/components/setup/SetupForm.test.js b/assets/js/modules/analytics-4/components/setup/SetupForm.test.js
index 05ac53f02a..b9e96ea43d 100644
--- a/assets/js/modules/analytics-4/components/setup/SetupForm.test.js
+++ b/assets/js/modules/analytics-4/components/setup/SetupForm.test.js
@@ -20,6 +20,7 @@
 import { act, fireEvent, render } from '../../../../../../tests/js/test-utils';
 import {
 	createTestRegistry,
+	createWaitForRegistry,
 	muteFetch,
 	provideModules,
 	provideSiteInfo,
@@ -486,7 +487,7 @@ describe( 'SetupForm', () => {
 				.dispatch( MODULES_ANALYTICS_4 )
 				.selectAccount( accountID );
 
-			const { getByRole, getByLabelText, waitForRegistry } = render(
+			let { getByRole, getByLabelText, waitForRegistry } = render(
 				<SetupForm />,
 				{
 					registry,
@@ -509,12 +510,12 @@ describe( 'SetupForm', () => {
 
 			expect( isEnhancedMeasurementEnabled ).toBe( false );
 
-			act( () => {
+			// An additional wait is required in order for all resolvers to finish.
+			await act( async () => {
+				waitForRegistry = createWaitForRegistry( registry );
 				triggerChange( getByRole );
+				await waitForRegistry();
 			} );
-
-			// An additional wait is required in order for all resolvers to finish.
-			await act( waitForDefaultTimeouts );
 		} );
 
 		it( 'should revert the enhanced measurement from off to on', () => {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks @aaemnnosttv - I had initially tried awaiting for the existing waitForRegistry() again, which didn't work - I hadn't considered created a new one. That looks like a useful pattern. We could apply it to other existing cases within this test file and no doubt in numerous other places in the codebase too.

// An additional wait is required in order for all resolvers to finish.
await act( waitForDefaultTimeouts );

// An additional wait is required in order for all resolvers to finish.
await act( async () => {
await waitForDefaultTimeouts();
} );

// An additional wait is required in order for all resolvers to finish.
await act( async () => {
await waitForDefaultTimeouts();
} );

@aaemnnosttv aaemnnosttv merged commit 1e6163b into develop Jan 17, 2025
19 of 23 checks passed
@aaemnnosttv aaemnnosttv deleted the enhancement/9008-remove-waitfor-actions branch January 17, 2025 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants