vitalets / playwright-network-cache

Cache and mock network requests in Playwright
MIT License
27 stars 2 forks source link

Roadmap #1

Open kalabro opened 2 months ago

kalabro commented 2 months ago

Hi there!

Really great idea, thank you for building playwright-network-cache!

We've tried version 0.1.3 and it seemed to work quite well. I particularly liked the simplicity of the approach and readability of cached files.

Given that the latest main includes breaking changes, would it be possible to share a draft roadmap so that we can watch the space and potentially contribute?

vitalets commented 2 months ago

Hey @kalabro Thanks for the feedback, that's important!

The main upcoming change is to replace global instance of cacheRoute with fixture usage. It provides more flexibility and more aligned with Playwright's router fixture. It will be a CacheRoute class that can instantiate a fixture:

import { CacheRoute } from 'playwright-network-cache';

const test = base.extend<{ cacheRoute: CacheRoute }>({
  cacheRoute: async ({ page }, use) => {
    await use(new CacheRoute(page));
  }
});

It allows to do useful things, like follows:

const test = base.extend<{ cacheRoute: CacheRoute }>({ cacheRoute: async ({ page }, use, testInfo) => { await use(new CacheRoute(page, { prefixDir: testInfo.project.name })); } });

* disable cache on retries:
```ts
import { CacheRoute } from 'playwright-network-cache';

const test = base.extend<{ cacheRoute: CacheRoute }>({
  cacheRoute: async ({ page }, use, testInfo) => {
    await use(new CacheRoute(page, { disabled: testInfo.retry > 0 }));
  }
});

API shape will also change, it will be close to the examples in the current README file. So instead of routeWithCache() there will be cacheRoute.GET() and similar methods. I will update readme accordingly.

Your contribution would be helpful on more details about your usage, as every project is unique. Some questions:

  1. Does this new design with fixture fits your setup?
  2. Do you update cached responses, and if yes - how often? Or you keep it forever, while tests are passing?
  3. Do you have cases, when you need different cache during one test (e.g. adding some item and return new list). How do you handle that?

Thanks in advance

kalabro commented 2 months ago

Thanks @vitalets!

We want to use playwright-network-cache for two reasons:

1) Global cache to improve overall performance and stability of end-to-end tests

We have several slow backend endpoints that are safe to cache due to the nature of our test suite.

Below is our fixture config to enable global caching:

app: async ({ page, baseURL }, use, testInfo) => {
        if (testInfo.retry > 1 || testInfo.tags.includes(TAGS.DISABLE_CACHE)) {
            console.log('test is running without global cache');
            return;
        }
        await routeWithCache(page, '/path1/*');
        await routeWithCache(page, '/path2/*');
        await routeWithCache(page, '/path3/*');

        // ... heavily modified by you get the idea
    },

We only want to cache GET requests, and the new API seems to be more friendly to this use case, which is great.

This setup works pretty well for us. We don't see crazy boost in time, but at least timing is more predictable.

So far the only issue I've seen are occasional SyntaxError: Unexpected end of JSON input or ENOENT: no such file or directory, open '.network-cache/localhost/path1/GET/body.json' errors at the very beginning of tests execution (we use multiple workers). When they occur, the corresponding test fails. Ideally, we would prefer a fallback to a network request instead.

I'm also unsure about the recommended local development workflow. So far we disable caching on local, but that may give us a fun time debugging flaky tests in CI.

Finally, we would like to know when in your opinion 'playwright-network-cache' will be stable enough for us to rely on it.

2) Mocking without maintaining mocks

We want to go away from MSW+Jest setup for so called "integration" tests in our frontend codebase. Instead, we started exploring what Playwright has to offer. This is how I found playwright-network-cache in one of the HAR issue threads.

Currently, we don't plan to use Playwright Component Testing mode. Instead, we want to reuse our existing e2e suite with all its page objects, helpers etc. The only issue - speed. My hope is that by serving all requests with JSON content type from cache and overriding a few relevant ones we can make such tests fast enough.

Your contribution would be helpful on more details about your usage, as every project is unique.

Sure!

  1. Does this new design with fixture fits your setup?

Yes, we actively use fixtures and the new setup will work even better for us.

  1. Do you update cached responses, and if yes - how often? Or you keep it forever, while tests are passing?

So far we update only one response, once.

Cached responses are valid for the entire test suite run (in an isolated CI pipeline).

  1. Do you have cases, when you need different cache during one test (e.g. adding some item and return new list). How do you handle that?

For the start, I completely skipped such tests from cache by tagging them with a special tag. As far as I understand, we can override cache handler instead.

I hope that gives you enough information to validate the next steps!

vitalets commented 2 months ago

Thanks @kalabro , noted both points! Like the idea of falling back in case of file reading error. Now the new API shape is defined, I'm already in progress. Let's stick with 0.1.3. I will update docs and notify you in this ticket when new version is out.

vitalets commented 2 months ago

Hi @kalabro ! I've released v0.2.0 with all the discussed changes. Updated readme as well. You are very welcome to check it and share the feedback.

The code you provided before could be updated like this:

import { CacheRoute } from 'playwright-network-cache';

app: async ({ page, baseURL }, use, testInfo) => {
        if (testInfo.retry > 1 || testInfo.tags.includes(TAGS.DISABLE_CACHE)) {
            console.log('test is running without global cache');
            return;
        }

+        const cacheRoute = new CacheRoute(page);
+        await cacheRoute.GET('/path1/*');
+        await cacheRoute.GET('/path2/*');
+        await cacheRoute.GET('/path3/*');
    },

To debug what's happening with cache under the hood, you can run it in debug mode:

DEBUG=playwright-network-cache npx playwright test
YuryMishin commented 1 month ago

I hope this message finds you well. I am a user of the playwright-network-cache package and I appreciate the great work you’ve done on it.

I would like to suggest a feature that I believe would enhance the caching capabilities of the library. Specifically, I propose adding an option to save the payload (or a hash of it) when making POST requests. This would allow for the creation of different caches for identical POST requests to the same URL with different payloads.

It would be ideal if this could be implemented as a flag, giving users the flexibility to enable or disable this feature as needed.

Thank you for considering my request. I look forward to your thoughts on this enhancement.

Best regards, Yury

vitalets commented 1 month ago

Hey Yuri!

Would not this help? I mean:

test('test', async ({ page, cacheRoute }) => {
  await cacheRoute.GET('/api/cats', {
    extraDir: req => req.postDataJSON().email // <- setup caching based on whatever fields from request body
  });
  // ...
});
YuryMishin commented 1 month ago

In some cases, POST requests contain large bodies, so calculating and storing a hash of the body might be a more efficient solution. However, I’m open to any other ideas you may have for handling this scenario effectively.

vitalets commented 1 month ago

Yes, we can calc hash inside extraDir as well:

test('test', async ({ page, cacheRoute }) => {
  await cacheRoute.GET('/api/cats', {
    extraDir: req => md5(req.postData())
  });
  // ...
});

I think it should be used carefully, because on every tiny change in request body new cache file will be generated.

YuryMishin commented 4 weeks ago

Currently, configuring caching requires specifying URLs in the configuration. It would be a fantastic enhancement if there was an option to enable global caching across all tests without needing to manually list each URL. This feature would streamline test setups and reduce configuration overhead, especially for larger test suites with numerous endpoints.

vitalets commented 4 weeks ago

Currently, configuring caching requires specifying URLs in the configuration.

Maybe it's not pointed well in the docs, but global options are available: they are passed when initializing cacheRoute fixture:

export const test = base.extend<{ cacheRoute: CacheRoute }>({
  cacheRoute: async ({ page }, use, testInfo) => {
    await use(new CacheRoute(page, {
      noCache: true // <- disables cache globally for all tests
    }));
  }
});
YuryMishin commented 3 weeks ago

I might not have expressed my suggestion clearly. What I’m hoping for is a way to set up caching globally so that all requests are automatically cached without needing to define the caching function individually in each test. This would simplify configuration and make caching setup more seamless across the entire test suite.

vitalets commented 3 weeks ago

What I’m hoping for is a way to set up caching globally so that all requests are automatically cached without needing to define the caching function individually in each test.

It is possible with auto fixture, check-out this sample.