?raw is empty under the test runner
A ?raw CSS import inlined fine in the app build but resolved to an empty string under the test runner, so tests exercised a rendering engine with no styles. Bundler-specific import mechanisms aren't guaranteed under your test runner — emit a real module the whole toolchain understands.
A new product in early scaffolding had a rendering engine that inlined
its own CSS into an iframe. The stylesheet reached the runtime through a
bundler ?raw import — the file’s text, imported as a string, injected
into the iframe’s srcdoc. It worked in the app build. Under the test
runner it resolved to an empty string, so every test drove the engine
with no styles at all: green, and meaningless. The fix was to stop
importing the asset and start generating it — the CSS emitted as a
first-class TypeScript module the whole toolchain reads the same way.
A loader plugin is not a language feature
?raw looks like a language feature. It isn’t — it’s a bundler
convention, resolved by a loader plugin that the app build happens to
have configured and the test runner does not. The two toolchains agreed
on the import syntax and disagreed on what it meant. Nothing errored,
because there’s nothing wrong with importing an empty string. The same
class of trap applies to import attributes, asset URLs, and any other
mechanism whose meaning lives in the bundler config rather than the
module graph.
Green-but-meaningless is worse than red
A failing test tells you where to look. A test that passes against a styleless engine tells you nothing while looking like coverage. The render path was fully exercised, the assertions ran, and the one thing that mattered — that the engine actually carried its styles — was silently absent. A test that can’t distinguish “styled” from “empty” is not testing the thing it was written to test.
Generate the module the whole toolchain understands
The resolution was to deliver the CSS as
iframe-engine-css.generated.ts — a generated module exporting the
stylesheet as a plain string constant, injected into the iframe exactly
as before. App build and test runner now read an identical, ordinary
import: no loader plugin in the path, no environment where it degrades.
When an asset must be byte-identical in production and in test, don’t
route it through a bundler affordance. Emit a real module and let every
tool in the chain agree on what it is.