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

Automatically mock css module imports #1512

Closed
4 tasks done
isaec opened this issue Jun 20, 2022 · 11 comments · Fixed by #1803
Closed
4 tasks done

Automatically mock css module imports #1512

isaec opened this issue Jun 20, 2022 · 11 comments · Fixed by #1803
Labels
enhancement New feature or request

Comments

@isaec
Copy link

isaec commented Jun 20, 2022

Clear and concise description of the problem

When using snapshots, any change to a module.(css|scss) file will change the generated class.
image
This creates a massive amount of noise in commits and pr. This random class name behavior is not desirable in a testing context, where you want predictability.

I would be open to submitting a PR for this issue if that was desired.

Suggested solution

The object imported from a module.(css|scss) file could be mocked to:

new Proxy(new Object(), {
  get(_, style) {
    return style;
  },
});

This means that when a class is accessed, it returns the raw string, reproducably:

import styles from "./Renderer.module.scss";
console.log(styles.underline) // "underline"

Instead of the occasionally randomized variant:

import styles from "./Renderer.module.scss";
console.log(styles.underline) // "_underline_i69w2_32" (until someone modifies Renderer.module.scss)

Alternative

This can be solved in each test file, by just mocking the module.

vi.mock("./Renderer.module.scss", () => ({
  default: new Proxy(new Object(), {
    get(_, style) {
      return style;
    },
  }),
}));

This is perfectly workable, but there is no reason a developer should need to do this - returning the same string every time is expected behavior in my opinion.

Additional context

Diffs where every class had its name changed (but nothing was actually changed) are massive, and can obscure actual changes - they suck. For example, if a pr includes a change to a css file and a modification to the classes of an object, the second change will be hidden behind the noise of the first, reducing the utility of the snapshot as a change detector.

Validations

@sheremet-va
Copy link
Member

sheremet-va commented Jun 20, 2022

We have a css property on test config. We might expose this functionality as test.css.* (open to naming, modules.keepClassNames?).

You can also use css option on Vite config to configure how the names are created.

@isaec
Copy link
Author

isaec commented Jun 20, 2022

Thanks for the prompt reply! The Vite config for how the names are created isn't useful because this behavior is desirable in production - the hashes are how collisions are prevented.

I would argue that keeping class names should be the default - test.css.modules.mangleClassNames? hashClassNames?

I can't think of a scenario where mangling the class names is desirable. If you are taking snapshots, it is definitely problematic. If you are matching elements with className, it is problematic to mangle the names unless you import the styles object, in which case you would be unaffected by this change.

It also doesn't seem like mangling can be turned off in Vite - it would defeat the collision prevention properties of modules.

@sheremet-va
Copy link
Member

The Vite config for how the names are created isn't useful because this behavior is desirable in production

You can apply it conditionally (using mode or process.env.VITEST). This is just another workaround.

I like mangleClassName idea.

Mangling can be useful if you want to limit access by class name (testing-library prohibits access by className), so this is why there should be a way to allow it. Mangling can be disabled by default, ofc.

@sheremet-va
Copy link
Member

Although I am not sure, if we should use new Proxy, or just configure generateScopedName. Because it kinda tests that you are using defined class. I think it might be useful to get undefined, if you are accessing class, that is not defined in CSS. This is debatable tho, since disable .module.css altogether might give you a boost in performance.

@isaec
Copy link
Author

isaec commented Jun 20, 2022

I would disagree that mangling allows you to limit access by className. If mangling is on for a test, I simply import the CSS module in the test to access the mangled name for matching.

On the aspect of proxy vs generate scoped name - I totally agree that generating a scoped name is better, it would be bad if there was different behavior in unit testing in that regard - undefined classes should return undefined.

This could also be achieved with a proxy that tests if the mocked object has the key, although I doubt there is a good reason to do such.

@janekkkk
Copy link

janekkkk commented Aug 31, 2022

@sheremet-va

process.env.VITEST

The Vite config for how the names are created isn't useful because this behavior is desirable in production

You can apply it conditionally (using mode or process.env.VITEST). This is just another workaround.

I like mangleClassName idea.

Mangling can be useful if you want to limit access by class name (testing-library prohibits access by className), so this is why there should be a way to allow it. Mangling can be disabled by default, ofc.

How would I be able to do this using mode or process.env.VITEST?

@sheremet-va
Copy link
Member

How would I be able to do this using mode or process.env.VITEST?

Something like:

export default defineConfig((mode) => {
  return {
    css: mode !== 'test' ? {} : {
      generateScopedName: (name) => name  
    }
  }
})

@rmjohnson-olo
Copy link

For anyone else that wants to use @sheremet-va's solution above, wanted to point out that generateScopedName is inside a modules property inside css. (took me a little while to figure out what I was doing wrong copying your example 😅 ) e.g.

export default defineConfig(({ mode }) => ({
    css: {
      modules:
        mode !== 'test'
          ? {}
          : {
              generateScopedName: name => name,
            },
    },
  }));

@sheremet-va
Copy link
Member

sheremet-va commented Sep 3, 2022

So, #1803 implements this logic:

  • By default, CSS processing is disabled for every css file
    • module.* exports a proxy in that case to not affect runtime (meaning, styles.{value} will always return a key with filename hash)
  • If processing is enabled (css.include), css is processed and injected into DOM
    • By default, module.* returns pseudo-scoped class names (styles.module always returns _module_${filenameHash}, styles.someRandomValue will return undefined)
    • If you want to enable original scoped names (so production and tests generate the same names), use css.modules.classNameStrategy: 'scoped'
  • Added css.modules.classNameStrategy option:
    • scoped - use css.modules.generateScopeName from user config or fallback to default behaviour, this is how it works in dev and prod
    • non-scoped - returns just the name
    • stable - returns the nama and hash, based on filepath, relative to root

@rmjohnson-olo
Copy link

Just wanted to say thanks @sheremet-va ! This new option is exactly what we needed. 🎉

@EvHaus
Copy link
Contributor

EvHaus commented Oct 11, 2022

In case anyone else runs into this issue in the future -- I got stuck here for while because I thought the option was:

export default defineConfig({
    css: {
        modules: {
            classNameStrategy: 'non-scoped'
        }
    }
})

But it's actually:

export default defineConfig({
    test: {
        css: {
            modules: {
                classNameStrategy: 'non-scoped'
            }
        }
    }
})

The first one has no effect.

@github-actions github-actions bot locked and limited conversation to collaborators Jun 12, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
5 participants