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 ember-cli-mirage to msw and @mswjs/data #10393

Open
wants to merge 188 commits into
base: main
Choose a base branch
from

Conversation

Turbo87
Copy link
Member

@Turbo87 Turbo87 commented Jan 15, 2025

As discussed in #10357, ember-cli-mirage isn't particularly well maintained anymore and blocking a couple of dependency updates.

This PR migrates us from using ember-cli-mirage for the mock API in the frontend test suites towards using msw and @mswjs/data.

I've used a pnpm workspace to add a subpackage at packages/crates-io-msw, that is roughly equivalent to the previous top-level mirage folder. This package contains:

  • all of the msw request handlers to simulate our crates.io API (exported as handlers)
  • the @mswjs/data database with all of the models that support the above request handlers (exported as db)
  • the data from mirage/fixtures/ and a loadFixtures() function

The package also contains its own dedicated test suite. The test suite is essentially a vitest port of the previous tests/mirage/ tests, but running in Node.js to simplify their development.

The @crates-io/msw package is integrated into the QUnit test suite by a setupMsw() function (similar to the previous setupMirage()). This function sets up the mock service worker and database for each test and ensures proper cleanup of both after the test has finished.

The package is also integrated into the Playwright test suite (at e2e/). Here, it is using a msw fixture, that can be used in tests and sets up playwright-msw which transforms the msw request handlers into Playwright request handlers. The msw fixture has three properties, worker is controlling the HTTP mocking, db is the @mswjs/data database mentioned above, and authenticateAs() is roughly equivalent to the similarly-named function from before. Since the mocking is now running on the Playwright side instead of inside the browser the test code could be simplified significantly.

I'm aware that the diff is big, and I'm happy to split it up into more manageable chunks if it helps. I definitely recommend reviewing in "ignore whitespace changes" mode. I've tried to group the commits thematically so that the can be reviewed in smaller chunks too. Let me know if/how I can help in getting this reviewed! :)

Closes #10357

Related:

@Turbo87 Turbo87 added A-frontend 🐹 C-internal 🔧 Category: Nonessential work that would make the codebase more consistent or clear labels Jan 15, 2025
@Turbo87 Turbo87 force-pushed the msw branch 5 times, most recently from 87a8963 to 400fa6c Compare January 17, 2025 10:35
@eth3lbert
Copy link
Contributor

eth3lbert commented Jan 18, 2025

Hmm, since msw is optional in @mswjs/data, I think we could use the webpack's externals trick to just let it get from the global (or other likes window, self, ...).
This would look something like following:

// ember-cli-build.js

...

  return require('@embroider/compat').compatBuild(app, Webpack, {
    extraPublicTrees,
    staticAddonTrees: true,
    staticAddonTestSupportTrees: true,
    staticModifiers: true,
    packagerOptions: {
      webpackConfig: {
        resolve: {
          fallback: {
            // disables `crypto` import warning in `axe-core`
            crypto: false,
            // disables `timers` import warning in `@sinon/fake-timers`
            timers: false,
          },
        },
        externals: ({ request, context }, callback) => {
          if (request == 'msw' && context.includes('@mswjs/data')) {
            return callback(null, request, 'global');
          }
          callback();
        },
      },
    },
  });

I'm not sure if this is more better than the patch approach though 😅

@Turbo87 Turbo87 force-pushed the msw branch 22 times, most recently from 7b13aa9 to 331b2d5 Compare January 27, 2025 10:09
@Turbo87 Turbo87 changed the title WIP: Migrate from ember-cli-mirage to msw and @mswjs/data Migrate from ember-cli-mirage to msw and @mswjs/data Jan 28, 2025
@Turbo87 Turbo87 requested a review from a team January 28, 2025 14:22
@Turbo87 Turbo87 marked this pull request as ready for review January 28, 2025 14:28
Copy link
Contributor

@eth3lbert eth3lbert left a comment

Choose a reason for hiding this comment

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

I haven't reviewed all the changes yet, just leaving a quick feedback after skimming through them. However, I love the move to msw, which seems much better than Mirage. Since it supports both browser and Node.js, it makes our E2E code assertions more natural :D

Copy link
Contributor

@LawnGnome LawnGnome left a comment

Choose a reason for hiding this comment

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

OK, so the caveat here is that my attitude towards reviewing test code is generally quite a bit laxer than it is towards production code, and my attitude towards meta-test code is laxer still. (Meta-meta-test code, however, overflows and I start taking it super seriously.)

Or, put another way, I didn't scrutinise every single line in a test case converted from the Mirage store API to the MSW database API. The tests succeeding is what I'm more interested in, honestly.

Overall, though, this feels like it would have been worth it even if ember-cli-mirage wasn't on the way out. Going from 160 globalThis references to 40 alone feels good. The MSW API is more intuitive to interact with. There's just, overall, less magic involved.

And, most importantly, all but one of the tests still pass for me locally. (I made a note on the one that didn't in the inline comments.)

In summary: let's do it. 👍

Comment on lines +54 to 55
- `packages/crates-io-msw` - A mock backend used for testing
- `node_modules/` - npm dependencies - (ignored in `.gitignore`)
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't suggest the actual right place in GitHub's UI, but this is less wrong:

Suggested change
- `packages/crates-io-msw` - A mock backend used for testing
- `node_modules/` - npm dependencies - (ignored in `.gitignore`)
- `node_modules/` - npm dependencies - (ignored in `.gitignore`)
- `packages/crates-io-msw` - A mock backend used for testing

packages/crates-io-msw/models/crate.js Show resolved Hide resolved
packages/crates-io-msw/models/msw-session.js Show resolved Hide resolved
e2e/acceptance/api-token.spec.ts Show resolved Hide resolved
Comment on lines +84 to +90
// Prevent `@mswjs/data` from bundling the `msw` package.
//
// `@crates-io/msw` is importing the ESM build of the `msw` package, but
// `@mswjs/data` is trying to import the CJS build instead. This is causing
// a conflict within webpack. Since we don't need the functionality within
// `@mswjs/data` that requires the `msw` package, we can safely ignore this
// import.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think I want to know how long it took you to figure this out.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah..... it took a while 😂

...handlers,
http.get('/assets/*', passthrough),
http.all(/.*\/percy\/.*/, passthrough),
http.get('https://:avatars.githubusercontent.com/u/:id', passthrough),
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the colon before the hostname correct here? I can't find an obvious reference to what this does in the MSW docs, but I also haven't read them exhaustively.

Secondary question: should we pass this through? It feels like this potentially makes tests sensitive to live avatar changes on GitHub.

Copy link
Member Author

Choose a reason for hiding this comment

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

GitHub uses multiple avatar subdomains and the colon is acting as a placeholder/wildcard.

without the pass-through we would be seeing images that don't load. we could potentially redirect it to an avatar image with the repo, but the pass-through is essentially just replicating the current behavior.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense, thanks!

we could potentially redirect it to an avatar image with the repo

I think we probably should do that, but not in this PR. 🙂

@@ -151,7 +151,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => {
await expect(page.locator('[data-test-api-token="1"] [data-test-expired-at]')).toHaveCount(0);
});

test('token expiry', async ({ page }) => {
test('token expiry', async ({ page, msw }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This test may have become locale-dependent in some cases:

  1) [chromium] › e2e/routes/settings/tokens/new.spec.ts:154:7 › /settings/tokens/new › token expiry @routes

    Error: Timed out 5000ms waiting for expect(locator).toHaveText(expected)

    Locator: locator('[data-test-expiry-description]')
    - Expected string  - 1
    + Received string  + 3

    - The token will expire on 18 February 2018
    +
    +           The token will expire on February 18, 2018
    +
    Call log:
      - expect.toHaveText with timeout 5000ms
      - waiting for locator('[data-test-expiry-description]')
        9 × locator resolved to <span data-test-expiry-description="" class="_expiry-description_6v2mdb">↵          The token will expire on February 18, …</span>
          - unexpected value "
              The token will expire on February 18, 2018
            "


      162 |     let expectedDate = expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' });
      163 |     let expectedDescription = `The token will expire on ${expectedDate}`;
    > 164 |     await expect(page.locator('[data-test-expiry-description]')).toHaveText(expectedDescription);
          |                                                                  ^
      165 |
      166 |     await page.fill('[data-test-name]', 'token-name');
      167 |     await page.locator('[data-test-expiry]').selectOption('none');
        at /home/adam/trees/rust-foundation/crates.io/e2e/routes/settings/tokens/new.spec.ts:164:66

  1 failed
    [chromium] › e2e/routes/settings/tokens/new.spec.ts:154:7 › /settings/tokens/new › token expiry @routes

Copy link
Member Author

Choose a reason for hiding this comment

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

huh, interesting. I'll see if I can replicate that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know which locale the headless browser will have chosen, honestly, but locale reports LC_TIME="en_AU.UTF-8".

Copy link
Contributor

@eth3lbert eth3lbert left a comment

Choose a reason for hiding this comment

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

I've glanced through the Playwright parts, they look solid to me.

Comment on lines +28 to +33
const row1 = page.locator('[data-test-row="0"]');
await expect(row1.locator('[data-test-crate-name]')).toHaveText(baz.name);
await expect(row1.locator('[data-test-description]')).toHaveText(baz.description);
const row0 = page.locator('[data-test-row="1"]');
await expect(row0.locator('[data-test-crate-name]')).toHaveText(bar.name);
await expect(row0.locator('[data-test-description]')).toHaveText(bar.description);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any reason why the variable names row0 and row1 are being swapped 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 rev deps are ordered by downloads and the factory defaults for the downloads have slightly changed because the data package is 1-indexed instead of 0-indexed like mirage 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I was just confused by the variable names and selectors. row0 mapped to [data-test-row="1"], and row1 mapped to [data-test-row="0"]. Alternatively, perhaps we could change the variable names to rowBar and rowBaz, which would be less confusing?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah I see what you mean. I'll fix the misalignment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-frontend 🐹 C-internal 🔧 Category: Nonessential work that would make the codebase more consistent or clear
Projects
Status: For next meeting
Development

Successfully merging this pull request may close these issues.

3 participants