-
Notifications
You must be signed in to change notification settings - Fork 56
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
Update react & react-dom to 18.3.0 and fix playwright flakiness #1807
Update react & react-dom to 18.3.0 and fix playwright flakiness #1807
Conversation
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.
Nice work on figuring this out! I think you've solved the problem, and the core of the solution seems to be a bunch of waitFor()
. However, I'm not sure all the Promise.all()
are needed, and I've elaborated on my hunch below.
@@ -22,12 +22,13 @@ test.describe('Campaigns list page ', () => { | |||
WelcomeNewMembers, | |||
]); | |||
|
|||
await page.goto(appUri + '/organize/1/projects'); | |||
const campaignCards = page.locator('data-testid=campaign-card'); |
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.
This (using locators instead of $$eval
) is a clear improvement IMO.
await Promise.all([ | ||
page.goto(appUri + '/organize/1/projects'), | ||
campaignCards.first().waitFor({ state: 'visible' }), | ||
]); |
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.
Is this really necessary though?
I've used the Promise.all([ someAction, someWait ])
construct a lot for cases where the two can happen simultaneously, e.g. where someAction
is a click or a navigation, and someWait
is, for example, an HTTP request that gets made as a result of that click. In those cases, the request can sometimes happen while the click evaluates, and when that happens the test would stall if we first waited for the click, and then waited for the request (which by then had already happened and won't happen again).
But in this situation, and others in this PR, I feel like the campaignCards.first().waitFor()
can always happen after the navigation. Isn't that right?
Here's what the documentation on waitFor
says:
Returns when element specified by locator satisfies the state option.
If target element already satisfies the condition, the method returns immediately. Otherwise, waits for up to timeout milliseconds until the condition is met.
That makes me think that if await page.goto(appUri + '/organize/1/projects')
finishes "late", once the campaign cards have already shown up, that won't be a problem because waitFor()
will just return immediately and the test can proceed.
If I'm right, it means we don't have to use the Promise.all()
construct here and I think that would be better as it would make the test more readable and "sequential".
await Promise.all([ | ||
page.goto(appUri + '/organize/1/journeys/1/1'), | ||
title.waitFor({ state: 'visible' }), | ||
breadcrumbTitle.waitFor({ state: 'visible' }), | ||
]); | ||
|
||
const numTitles = await title.count(); | ||
const numBreadcrumbTitles = await breadcrumbTitle.count(); | ||
|
||
expect(numTitles).toEqual(1); | ||
expect(numBreadcrumbTitles).toEqual(1); |
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 an example, I think the important part of the fix here is the added uses of waitFor()
, not the Promise.all()
. I'm pretty sure you could do this instead?
await page.goto(appUri + '/organize/1/journeys/1/1');
await title.waitFor({ state: 'visible' });
await breadcrumbTitle.waitFor({ state: 'visible' });
const numTitles = await title.count();
const numBreadcrumbTitles = await breadcrumbTitle.count();
expect(numTitles).toEqual(1);
expect(numBreadcrumbTitles).toEqual(1);
Do you agree that this pattern would make the code more readable?
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.
You're 100% correct here I think. Reckon I've accidentally picked this up from those other ones you mentioned and not really thought much about it. Crossed my mind briefly at one point I think, but guess I got a little lost in the fun here. Will make this change real quick, thanks for raising this!
One more thing! Something seems to have changed about the fonts in this PR. The GIF below compares the current UPDATE: It looks like this code in <script>{'try{Typekit.load({ async: true })}catch(e){}'}</script> |
I've re-run |
Okay the font issue is a pain! It doesn't reproduce in the dev server!! I've tried running I'm tempted to push a commit along these lines into a branch and use the Vercel preview builds to check if it fixes the issue. Seems like you set those things up at the exact right time lol. - <script>{'try{Typekit.load({ async: true })}catch(e){}'}</script>
+ <script
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{
+ __html: 'try{Typekit.load({ async: true })}catch(e){}',
+ }}
+ /> Reading the TypeKit docs at https://helpx.adobe.com/fonts/using/embed-codes.html#Changingtheembedcodeinawebsite, it sounds to me like this is an older form of embed code anyway. Another option here could be to try using the |
Try just using the
Yeah, I saw that, and if you want to try that change I'd be happy to make the switch. I'm not sure we will be using typekit much longer anyway. We'll see what happens with our current design system work, but for now it would be nice to fix this issue so that we will be able to merge this branch before that work is complete (which will be months). |
Okay cool, I ran with TypeKit's most up-to-date embed code in 1b40c75 and then used your handy advice about running
Seems to be working very nicely! |
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.
This is amazing. I've looked at this again because I'm frustrated that tests in other PRs seem flakier than usual, and I am almost ready to merge, but I have some comments below that I would like to discuss before doing so.
]); | ||
|
||
moxy.setZetkinApiMock(`/orgs/1/people/${ClaraZetkin.id}/tags`, 'get', []); | ||
|
||
await tagToDelete.nth(2).waitFor({ state: 'hidden' }); |
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 nth(2)
feels a bit risky. Is this because the title of the tag is "Active" and that string also occurs in other places on the page?
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.
This is now await tagToDelete.waitFor({ state: 'hidden' });
thanks to changing the tag from Activist
to Plays guitar
.
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 get this thing merged! I believe in us. 🙏
Description
The new survey page depends on
useFormState
which depends on the canary release channel versions ofreact
andreact-dom
. The new version ofreact-dom
causes mayhem with the Playwright tests.The new survey page is a big enough change in its own right. I wanted to separate this other work into this PR in order to get that solved and unblock the survey page.
Screenshots
None.
Changes
Full excruciating details of the process of producing this PR are available at henrycatalinismith#1.
My hunch is that react-dom 18.3.0's performance characteristics are somehow different than before. I think this exposes pre-existing race conditions in our playwright tests.
The rough idea of my fixes here is that I'm splitting up
page.locator('…').something()
chains. Instead I'm doingawait locator.waitFor({ state: 'visible' })
on the locators to ensure the elements they refer to have rendered before allowing the tests to progress to the next phase that depends on those elements being visible to dosomething()
with them.Generally speaking, I'm adding a lot of
Promise.all
blocks containing a pair of promises where the first one is doing something (e.g. clicking a button, pressing a key) and the second is observing the outcome (e.g. waiting for a menu to open). It's a structure that sets a nice steady drumbeat to the progression of the tests which seems to help keep things from getting tangled up in each other.Notes to reviewer
I've been doing a lot of
yarn playwright:skipbuild --repeat-each=4
, personally. If you're feeling patient, crank that4
up to a bigger number, go make yourself a coffee while it runs, and see if you turn up anything interesting.Related issues
None.