-
-
Notifications
You must be signed in to change notification settings - Fork 13
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
Migrate from Jest to Playwright #62
Conversation
Socket Security Pull Request Report👍 No new dependency issues detected in pull request Pull request report summary
Bot CommandsTo ignore an alert, reply with a comment starting with Ignoring: Powered by socket.dev |
9d32ea3
to
2312b49
Compare
@SocketSecurity ignore playwright@1.30.0 The install script is legitimate, and we have it disabled anyway. The bin confusion is a similar situation to Jest; multiple packages pointing to the same place |
dfc705c
to
651afa9
Compare
a5693f0
to
5616e34
Compare
This comment was marked as resolved.
This comment was marked as resolved.
8b48149
to
ca3a5ac
Compare
c2e6460
to
9d277c2
Compare
The Jest tests have been completely replaced by e2e tests written using Playwright. Jest is not well suited for testing the behavior of this website. Too much of the code-under-test was related to DOM/browser behavior that is not adequately mocked by Jest's `jsdom` environment, and there was no way to fully reset in between each test to prevent them from interfering with each other. As a result, most of the functionality was not being tested. We should now have nearly complete coverage. The tests have been split into modules, one for each use-case. The dependency `http-server` was added to serve the site locally during development, and in CI. The README has been updated with some basic instructions, but there is still much room for improvement there. The Playwright config was auto-generated by Playwright's setup command, with minimal changes made. The change from default were: * Set a `baseURL` * Uncomment Edge and Google Chrome * Set the `webServer` config
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.
Wow — was Playwright always this good?! Either way, these tests are excellent. I like how simple and readable they are. I had some questions, but I didn't see anything that is absolutely necessary to change here.
Before running tests using Playright, you will need to install the required browsers using this command: | ||
|
||
``` | ||
yarn playwright install --with-deps chrome chromium firefox msedge |
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 kind of wish that we'd kept the yarn setup
convention for this and other similar use cases, but I guess this makes sense as a one-off case. Might be something that people gloss over though. Is it worth it to figure out a way to bake it into yarn install
? Or maybe we start employing postinstall
again?
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 kinda see this being a separate step as a feature rather than a bug. This is an arduous install process; huge download sizes and long install times, and all totally unnecessary unless you're working with e2e tests.
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 just improved these instructions: 160154a
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.
Oh, okay. If you're working on this repo, wouldn't you want to run all tests locally, including the e2e tests? But, I see your point. Someone would probably have to read the README to understand how the e2e tests work, anyway, so that's fair to have a separate step.
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.
Personally I've only been testing with firefox and chromium. There was an issue with installing Google Chrome that I hadn't bothered look into. I could understand contributors maybe only testing with one or two browsers, or not bothering test at all if their contribution isn't changing any test expectations.
await page.goto(`/#${querystring}`); | ||
|
||
const popupPromise = page.waitForEvent('popup'); | ||
await page.getByRole('link', { name: 'report a detection problem' }).click(); |
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 looks eerily similar to Testing Library. What great inspiration.
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.
Yep, they directly credited testing-library in the release notes for this feature: https://github.com/microsoft/playwright/releases/tag/v1.27.0
}); | ||
await page.goto(`/#${querystring}`); | ||
|
||
const popupPromise = page.waitForEvent('popup'); |
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 wait for a popup to appear by listening to the "popup" event... how clever. Makes perfect sense to me!
}); | ||
await page.goto(`/#${querystring}`); | ||
|
||
await page.locator('css=#unsafe-continue').click(); |
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.
Interesting. Wonder why they accept a prefixed string rather than an object in this case. Ah, well, it's readable just the same.
targetOrigin, | ||
); | ||
} else if (message.data !== 'ACK') { | ||
window.logCall(message.data, targetOrigin); |
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.
It looks like logCall
does not exist and is not referenced anywhere else but here, so I'm curious, why define it globally? Is it possible to say logs.push({ message: message.data, targetOrigin })
here?
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 function passed to addInitScript
is serialized and executed in the window-under-test itself, so it wouldn't have access to the logs
variable.
logCall
is defined using page.exposeFunction
a few lines up from here. exposeFunction
basically lets you execute a function in the context of the test from the page. window.logCall
ends up messaging the test-runner with the serialized parameters, and calling the callback passed to exposeFunction
.
This solution was developed from an example in their documentation: https://playwright.dev/docs/mock-browser-apis#verifying-api-calls
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.
Some comments added here to help explain this: 2355649
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 function passed to
addInitScript
is serialized and executed in the window-under-test itself, so it wouldn't have access to thelogs
variable.
Got it. That was the key thing here. Thanks! I also appreciate the comments as well.
tests/bypass.spec.ts
Outdated
|
||
await expect(page).toHaveURL('https://test.com'); | ||
await expect(postMessageLogs.length).toBe(1); | ||
await expect([postMessageLogs[0].message]).toStrictEqual([ |
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.
Nit: Why wrap this in an array?
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.
Oops! This was a mistake
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.
Fixed here: c065dfa
*/ | ||
export async function setupDefaultMocks(context) { | ||
await context.route('https://github.com/**/*', (route) => | ||
route.fulfill({ |
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.
What a great API. Love these names.
// on `about:blank`. | ||
// We need a 1-length history so that the browser allows `window.close()` to work. | ||
const baseURL = config.use?.baseURL; | ||
await page.evaluate((url) => window.location.replace(url), `${baseURL}/`); |
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.
Nit: Does a URL need to be passed into this function? Wondering if it would be more clear to build it inside.
await page.evaluate((url) => window.location.replace(url), `${baseURL}/`); | |
await page.evaluate(() => window.location.replace(`${baseURL}/`)); |
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.
We need to pass in the URL because this function is serialized and executed on the page itself, so it doesn't have access to locally-scoped variables like baseURL
.
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.
Ah. Same story as addInitScript
from above, I guess.
}); | ||
const expectedMessage = 'Service worker registered!'; | ||
|
||
await page.goto('/#extensionStartup'); |
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.
Interesting. When is #extensionStartup
usually accessed? Is that something that the browser does automatically?
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 extension loads this during startup, so that the phishing-warning page service worker is registered before the user hits their first warning page. This ensures it can work offline (e.g. if GitHub pages is down), so we're not as reliant on hosting for this feature.
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.
Cool! Thanks for the explanation.
await setupDefaultMocks(context); | ||
}); | ||
|
||
test('does not render any buttons or links', async ({ 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.
Curious — what code is this testing? What hides user interactions?
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.
We have an "antiClickjack" style block that ensures the main contents of the warning page are hidden by default, including the buttons at links. This is to ensure a malicious site cannot load the phishing warning page in an iframe and trick the user into bypassing the warning page for their other malicious site.
There is a little script embedded directly in the HTML document that checks to see if this is the top frame, and if so it modifies that style block to no longer hide 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.
I'm just noticing that this script references a now-removed HTML element (#content__framed-body
). That was an oversight. But it still effectively works in that it un-hides 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.
Ah, I see, it's this bit, it looks like. Makes sense!
The Jest tests have been completely replaced by e2e tests written using Playwright.
Jest is not well suited for testing the behavior of this website. Too much of the code-under-test was related to DOM/browser behavior that is not adequately mocked by Jest's
jsdom
environment, and there was no way to fully reset in between each test to prevent them from interfering with each other. As a result, most of the functionality was not being tested.We should now have nearly complete coverage. The tests have been split into modules, one for each use-case.
The dependency
http-server
was added to serve the site locally during development, and in CI.The README has been updated with some basic instructions, but there is still much room for improvement there.
The Playwright config was auto-generated by Playwright's setup command, with minimal changes made. The change from default were:
baseURL
webServer
config