Files
oxc/crates/oxc_formatter_css/tests/fixtures/mod.rs
T
Yuji Sugiura 296623d868 feat(oxfmt)!: format parser:html files by oxc_formatter_htmlish (#28273)
Fixes #28263 

- Before this PR: `.html` and also html-in-{js,md,etc} are formatted by
Prettier.
  - Because of that, we had to run JS, which became an overhead.
- After this PR: All of them are formatted natively by
`oxc_formatter_htmlish`.
  - No overhead.

At the same time, since we are fixing Prettier bugs and inconsistent
behavior, please refer to DIVERGENCES.md for the differences.
2026-10-30 16:29:13 +09:00

186 lines
7.2 KiB
Rust

//! Fixture tests for cases the Prettier conformance suite does NOT cover,
//! plus minimal re-pins of known divergences the suite DOES hit
//! (a divergence needs its own pin; the big conformance file is only a regression net).
//!
//! Expected outputs were verified against `prettier` by hand;
//! when adding a fixture, do the same (`npx prettier@<oxfmt-bundle-version> --parser <variant>`).
use std::path::Path;
use oxc_allocator::{Allocator, ArenaVec};
use oxc_formatter_css::{CssFormatOptions, CssRoot, CssVariant, format, parse_for_format};
use oxc_formatter_tests::{FixtureFormatter, OptionSet, build_fixture_snapshot};
mod options;
use options::apply_css_options;
struct CssHarness;
/// What formatting must leave unchanged, see `FixtureFormatter::Fingerprint`.
#[derive(Debug, PartialEq)]
struct Fingerprint {
comments: usize,
}
impl FixtureFormatter for CssHarness {
type Options = CssFormatOptions;
type Fingerprint = Fingerprint;
fn parse_options(json: &OptionSet) -> Self::Options {
let mut options = CssFormatOptions::default();
apply_css_options(&mut options, json);
options
}
fn format(source: &str, path: &Path, options: &Self::Options) -> String {
let options = CssFormatOptions { variant: variant_of(path), ..*options };
if is_embedded(path) {
return format_embedded(source, options);
}
let allocator = Allocator::default();
format(&allocator, source, options)
.expect("format should succeed")
.print()
.expect("print should succeed")
.into_code()
}
fn fingerprint(source: &str, path: &Path, options: &Self::Options) -> Fingerprint {
let options = CssFormatOptions { variant: variant_of(path), ..*options };
let allocator = Allocator::default();
let parsed = parse_for_format(
&allocator,
source,
options,
if is_embedded(path) { CssRoot::CssInJsTemplate } else { CssRoot::Stylesheet },
)
.expect("source should parse");
Fingerprint { comments: parsed.comments.len() }
}
}
/// The dialect comes from the fixture extension, like oxfmt's classifier.
fn variant_of(path: &Path) -> CssVariant {
match path.extension().and_then(|e| e.to_str()) {
Some("scss") => CssVariant::Scss,
Some("less") => CssVariant::Less,
_ => CssVariant::Css,
}
}
/// Fixtures under `embedded/` exercise the dispatcher entry point
/// (`format_to_ir`, css-in-js), which tolerates
/// `` `PLACEHOLDER-N` `` markers in value/selector position.
fn is_embedded(path: &Path) -> bool {
path.components().any(|c| c.as_os_str() == "embedded")
}
/// Format through `format_to_ir` and print the raw IR, mirroring what the
/// oxfmt dispatcher + parent template printing do (minus `${}` substitution).
fn format_embedded(source: &str, options: CssFormatOptions) -> String {
use oxc_formatter_core::{Document, FormatElement, FormatOptions};
let allocator = Allocator::default();
let session =
oxc_formatter_core::FormatSession::new(&allocator, oxc_formatter_core::InputKind::Fragment);
let embedded = oxc_formatter_css::format_to_ir(
&session,
source,
options,
Some(oxc_formatter_css::CssEmbeddedIn::CssInJsTemplate),
)
.expect("format should succeed");
// Simulate the host: replace each typed placeholder with the canonical
// sentinel (the real host substitutes `${expr}`; tests have no expressions).
// The printer `debug_assert`s on any surviving `EmbedPlaceholder`.
let elements = ArenaVec::from_iter_in(
embedded.ir.iter().map(|element| match element {
FormatElement::EmbedPlaceholder(index) => {
let text = allocator.alloc_str(&std::format!("`PLACEHOLDER-{index}`"));
FormatElement::text(text, options.indent_width)
}
other => other.clone(),
}),
&session.allocator(),
);
let mut code = Document::new(elements, Vec::new())
.print(source.len(), options.as_print_options())
.expect("print should succeed")
.into_code();
// The embedded entry point emits no trailing newline (the parent owns it);
// add one so snapshots stay diff-friendly.
code.push('\n');
code
}
fn test_file(path: &Path) {
// `insta::assert_snapshot!` is invoked from this file so the snapshot's
// `source:` header records this consumer crate, not the shared harness.
let snap = build_fixture_snapshot::<CssHarness>(path);
insta::with_settings!({
snapshot_path => snap.path,
prepend_module_to_snapshot => false,
snapshot_suffix => "",
omit_expression => true,
}, {
insta::assert_snapshot!(snap.name, snap.body);
});
}
// Include auto-generated test functions from build.rs
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));
// ---
#[test]
fn bom_is_preserved() {
let allocator = Allocator::default();
let formatted =
format(&allocator, "\u{feff}a {\n color: red;\n}\n", CssFormatOptions::default())
.expect("BOM input should parse")
.print()
.expect("print should succeed")
.into_code();
assert_eq!(formatted, "\u{feff}a {\n color: red;\n}\n");
}
#[test]
fn parse_error_is_err() {
let allocator = Allocator::default();
let css = CssFormatOptions::default();
let scss = CssFormatOptions { variant: CssVariant::Scss, ..css };
let less = CssFormatOptions { variant: CssVariant::Less, ..css };
for (source, options) in [
// Root declaration: `TopLevelDeclaration` in a stylesheet, as opposed to a fragment (`CssRoot`)
// (dart-sass rejects it; Less parses it like less.js).
("display: flex;", scss),
// EOF/newline-unclosed constructs: oxc-css-parser (0.0.6+) recovers to a
// valid AST but records the spec parse error, so they bail like every
// other recoverable error. Prettier rejects all of these too ("Unclosed
// block" / "Unclosed string" / "Unclosed bracket"); formatting them
// would corrupt the input (`;` appended inside the unclosed construct).
("a {\n color: red;\n", css),
("@media (min-width: 500px) {\n", css),
("a { content: \"abc", css),
("a {\n content: \"\n}", css),
("a { width: calc(100% - 10px", css),
("a { --x: {", css),
// css-in-js `${}` markers in value position...
("a { color: `PLACEHOLDER-0`; }", scss),
// ...and in selector position stay errors in the STANDALONE entry
// (`format_to_ir` tolerates them via the oxc-css-parser fork option;
// see `fixtures/embedded/`).
(".a-`PLACEHOLDER-0` {\n}", scss),
// The raw-prelude fallback (`unknown-qualified-rule.css`) fires only
// when a declaration's value hits a top-level `{` (name + `:` parsed);
// a selector error with no such head stays a hard error.
("a, { color: red; }", css),
// Pin for crates/oxc_formatter_css/DIVERGENCES.md#less-value-interpolation-rejected
(".a { width: @{min-width}; }", less),
] {
assert!(format(&allocator, source, options).is_err(), "{source:?} should fail to format");
}
}