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

Migrate from Jest to Playwright #62

Merged
merged 5 commits into from
Feb 17, 2023
Merged

Migrate from Jest to Playwright #62

merged 5 commits into from
Feb 17, 2023

Conversation

Gudahtt
Copy link
Member

@Gudahtt Gudahtt commented Feb 16, 2023

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

@socket-security
Copy link

socket-security bot commented Feb 16, 2023

Socket Security Pull Request Report

👍 No new dependency issues detected in pull request

Pull request report summary
Issue Status
Install scripts ✅ 0 issues
Native code ✅ 0 issues
Bin script confusion ✅ 0 issues
Bin script shell injection ✅ 0 issues
Unresolved require ✅ 0 issues
Invalid package.json ✅ 0 issues
HTTP dependency ✅ 0 issues
Git dependency ✅ 0 issues
Potential typo squat ✅ 0 issues
Known Malware ✅ 0 issues
Telemetry ✅ 0 issues
Protestware/Troll package ✅ 0 issues
Bot Commands

To ignore an alert, reply with a comment starting with @SocketSecurity ignore followed by a space separated list of package-name@version specifiers. e.g. @SocketSecurity ignore foo@1.0.0 bar@2.4.2

Ignoring: playwright@1.30.0, @playwright/test@1.30.0, playwright-core@1.30.0

Powered by socket.dev

@Gudahtt Gudahtt force-pushed the playwright branch 3 times, most recently from 9d32ea3 to 2312b49 Compare February 16, 2023 20:15
@Gudahtt
Copy link
Member Author

Gudahtt commented Feb 16, 2023

@SocketSecurity ignore playwright@1.30.0
@SocketSecurity ignore @playwright/test@1.30.0
@SocketSecurity ignore playwright-core@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

@Gudahtt Gudahtt force-pushed the playwright branch 5 times, most recently from dfc705c to 651afa9 Compare February 16, 2023 20:56
Base automatically changed from fix-detection-repo-initialization to main February 16, 2023 21:08
@Gudahtt Gudahtt changed the base branch from main to update-github-actions February 16, 2023 21:21
@Gudahtt Gudahtt force-pushed the playwright branch 3 times, most recently from a5693f0 to 5616e34 Compare February 16, 2023 22:03
@Gudahtt

This comment was marked as resolved.

@Gudahtt Gudahtt force-pushed the update-github-actions branch from 8b48149 to ca3a5ac Compare February 16, 2023 23:14
Base automatically changed from update-github-actions to main February 17, 2023 14:26
@Gudahtt Gudahtt force-pushed the playwright branch 2 times, most recently from c2e6460 to 9d277c2 Compare February 17, 2023 16:44
@Gudahtt Gudahtt marked this pull request as ready for review February 17, 2023 16:44
@Gudahtt Gudahtt requested a review from a team as a code owner February 17, 2023 16:44
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
mcmire
mcmire previously approved these changes Feb 17, 2023
Copy link
Contributor

@mcmire mcmire left a 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
Copy link
Contributor

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?

Copy link
Member Author

@Gudahtt Gudahtt Feb 17, 2023

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.

Copy link
Member Author

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

Copy link
Contributor

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.

Copy link
Member Author

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();
Copy link
Contributor

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.

Copy link
Member Author

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');
Copy link
Contributor

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();
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Member Author

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

Copy link
Contributor

@mcmire mcmire Feb 17, 2023

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.

Got it. That was the key thing here. Thanks! I also appreciate the comments as well.


await expect(page).toHaveURL('https://test.com');
await expect(postMessageLogs.length).toBe(1);
await expect([postMessageLogs[0].message]).toStrictEqual([
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Member Author

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({
Copy link
Contributor

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}/`);
Copy link
Contributor

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.

Suggested change
await page.evaluate((url) => window.location.replace(url), `${baseURL}/`);
await page.evaluate(() => window.location.replace(`${baseURL}/`));

Copy link
Member Author

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.

Copy link
Contributor

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');
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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 }) => {
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Contributor

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!

@Gudahtt Gudahtt merged commit 3d48389 into main Feb 17, 2023
@Gudahtt Gudahtt deleted the playwright branch February 17, 2023 21:43
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.

2 participants