Skip to content

refactor: adding further fixtures #40

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,76 @@ npm run serve
# Lint code
npm run lint
```

### Development Scripts

The project includes several utility scripts in the `scripts/` directory to help with development and maintenance:

#### `scripts/build-docs.js`

Automatically generates documentation by injecting CSS test fixtures into markdown files.

```bash
node scripts/build-docs.js
```

This script:

- Scans for `<!-- FIXTURE: fixture-name -->` placeholders in markdown files
- Replaces them with the corresponding input/expected CSS from `test/fixtures/`
- Updates documentation files like `docs/TEST_FIXTURES.md` and package READMEs

#### `scripts/generate-fixtures.js`

Utility script for generating expected output files for new test fixtures by running them through the polyfill transformation engine.

```bash
node scripts/generate-fixtures.js
```

This script:

- Reads CSS from `.input.css` files in `test/fixtures/`
- Runs them through the `buildTimeTransform` function
- Generates corresponding `.expected.css` files
- Useful when adding new test cases to ensure correct expected outputs

**Note:** This script is primarily for development use when creating new fixtures. Modify the `newFixtures` array in the script to specify which fixtures to process.

### Test Fixture System

The project uses a centralized test fixture system located in `test/fixtures/` with pairs of `.input.css` and `.expected.css` files. This system is managed through `test/scripts/fixture-utils.js`.

#### Adding New Test Fixtures

1. **Create fixture files:**

```text
test/fixtures/my-new-feature.input.css
test/fixtures/my-new-feature.expected.css
```

2. **Add to fixture arrays in `test/scripts/fixture-utils.js`:**
- `basicFixtureTests` - for build-time transformable features
- `runtimeOnlyFixtureTests` - for runtime-only features
- `postcssFixtureTests` - for PostCSS plugin tests

3. **Generate expected outputs (optional):**

```bash
# Modify scripts/generate-fixtures.js to include your fixture
node scripts/generate-fixtures.js
```

4. **Update documentation:**
```bash
node scripts/build-docs.js
```

#### Fixture Categories

- **Build-time transformable**: Features that can be converted to native CSS (`@media`, `@supports`)
- **Runtime-only**: Features requiring JavaScript processing (`style()` queries, boolean negation, empty token streams)
- **PostCSS-specific**: Additional fixtures used only by the PostCSS plugin

For more details, see [`docs/TEST_FIXTURES.md`](docs/TEST_FIXTURES.md).
2 changes: 1 addition & 1 deletion docs/TEST_FIXTURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ This document demonstrates the centralized test fixture system that provides a s

```css
.test {
color: if(style(--theme): var(--primary) ; else: blue);
color: if(style(--theme): var(--primary); else: blue);
}
```

Expand Down
19 changes: 18 additions & 1 deletion packages/css-if-polyfill/test/integrated.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import {
basicFixtureTests,
loadFixture,
normalizeCSS
normalizeCSS,
runtimeOnlyFixtureTests
} from '../../../test/scripts/fixture-utils.js';
import { buildTimeTransform, init, processCSSText } from '../src/index.js';

Expand Down Expand Up @@ -30,6 +31,22 @@
});
}

// Runtime-only fixtures that cannot be transformed at build time
for (const { fixture, description } of runtimeOnlyFixtureTests) {
test(description, () => {
const { input, expected } = loadFixture(fixture);
const result = buildTimeTransform(input);

// These fixtures should preserve the if() functions for runtime processing
expect(result.hasRuntimeRules).toBe(true);
expect(normalizeCSS(result.runtimeCSS)).toBe(

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Checks

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle multiple mixed condition types in single if() function

AssertionError: expected '.responsive{padding: if( media(width …' to be '.responsive{padding: 15px;}@supports …' // Object.is equality Expected: ".responsive{padding: 15px;}@supports (display: grid){.responsive{padding: 25px;}}@media (width >= 768px){.responsive{padding: 30px;}}@media (width >= 1200px){.responsive{padding: 40px;}}" Received: ".responsive{padding: if( media(width >= 1200px): 40px;media(width >= 768px): 30px;supports(display: grid): 25px;style(--large-padding): 35px;else: 15px );}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Checks

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle boolean negation in conditions

AssertionError: expected '.test{color: if(not media(print): blu…' to be '.test{color: black;}@media (not (prin…' // Object.is equality Expected: ".test{color: black;}@media (not (print)){.test{color: blue;}}.test{display: grid;}@supports (not display: grid){.test{display: block;}}" Received: ".test{color: if(not media(print): blue;else: black);}.test{display: if(not supports(display: grid): block;else: grid);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Checks

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > prevent cyclic substitution context in style queries

AssertionError: expected '.test{--foo: if(style(--foo: bar): ba…' to be '.test{--foo: default;}' // Object.is equality Expected: ".test{--foo: default;}" Received: ".test{--foo: if(style(--foo: bar): baz;else: default);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Checks

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle if() functions without else clause (empty token stream)

AssertionError: expected '.test{color: if(media(print): red);}.…' to be '.test{background: transparent;}' // Object.is equality Expected: ".test{background: transparent;}" Received: ".test{color: if(media(print): red);}.test{background: if(supports(display: none): transparent);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Analysis

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle multiple mixed condition types in single if() function

AssertionError: expected '.responsive{padding: if( media(width …' to be '.responsive{padding: 15px;}@supports …' // Object.is equality Expected: ".responsive{padding: 15px;}@supports (display: grid){.responsive{padding: 25px;}}@media (width >= 768px){.responsive{padding: 30px;}}@media (width >= 1200px){.responsive{padding: 40px;}}" Received: ".responsive{padding: if( media(width >= 1200px): 40px;media(width >= 768px): 30px;supports(display: grid): 25px;style(--large-padding): 35px;else: 15px );}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Analysis

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle boolean negation in conditions

AssertionError: expected '.test{color: if(not media(print): blu…' to be '.test{color: black;}@media (not (prin…' // Object.is equality Expected: ".test{color: black;}@media (not (print)){.test{color: blue;}}.test{display: grid;}@supports (not display: grid){.test{display: block;}}" Received: ".test{color: if(not media(print): blue;else: black);}.test{display: if(not supports(display: grid): block;else: grid);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Analysis

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > prevent cyclic substitution context in style queries

AssertionError: expected '.test{--foo: if(style(--foo: bar): ba…' to be '.test{--foo: default;}' // Object.is equality Expected: ".test{--foo: default;}" Received: ".test{--foo: if(style(--foo: bar): baz;else: default);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Code Quality Analysis

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle if() functions without else clause (empty token stream)

AssertionError: expected '.test{color: if(media(print): red);}.…' to be '.test{background: transparent;}' // Object.is equality Expected: ".test{background: transparent;}" Received: ".test{color: if(media(print): red);}.test{background: if(supports(display: none): transparent);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (24)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle multiple mixed condition types in single if() function

AssertionError: expected '.responsive{padding: if( media(width …' to be '.responsive{padding: 15px;}@supports …' // Object.is equality Expected: ".responsive{padding: 15px;}@supports (display: grid){.responsive{padding: 25px;}}@media (width >= 768px){.responsive{padding: 30px;}}@media (width >= 1200px){.responsive{padding: 40px;}}" Received: ".responsive{padding: if( media(width >= 1200px): 40px;media(width >= 768px): 30px;supports(display: grid): 25px;style(--large-padding): 35px;else: 15px );}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (24)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle boolean negation in conditions

AssertionError: expected '.test{color: if(not media(print): blu…' to be '.test{color: black;}@media (not (prin…' // Object.is equality Expected: ".test{color: black;}@media (not (print)){.test{color: blue;}}.test{display: grid;}@supports (not display: grid){.test{display: block;}}" Received: ".test{color: if(not media(print): blue;else: black);}.test{display: if(not supports(display: grid): block;else: grid);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (24)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > prevent cyclic substitution context in style queries

AssertionError: expected '.test{--foo: if(style(--foo: bar): ba…' to be '.test{--foo: default;}' // Object.is equality Expected: ".test{--foo: default;}" Received: ".test{--foo: if(style(--foo: bar): baz;else: default);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (24)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle if() functions without else clause (empty token stream)

AssertionError: expected '.test{color: if(media(print): red);}.…' to be '.test{background: transparent;}' // Object.is equality Expected: ".test{background: transparent;}" Received: ".test{color: if(media(print): red);}.test{background: if(supports(display: none): transparent);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (22)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle multiple mixed condition types in single if() function

AssertionError: expected '.responsive{padding: if( media(width …' to be '.responsive{padding: 15px;}@supports …' // Object.is equality Expected: ".responsive{padding: 15px;}@supports (display: grid){.responsive{padding: 25px;}}@media (width >= 768px){.responsive{padding: 30px;}}@media (width >= 1200px){.responsive{padding: 40px;}}" Received: ".responsive{padding: if( media(width >= 1200px): 40px;media(width >= 768px): 30px;supports(display: grid): 25px;style(--large-padding): 35px;else: 15px );}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (22)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle boolean negation in conditions

AssertionError: expected '.test{color: if(not media(print): blu…' to be '.test{color: black;}@media (not (prin…' // Object.is equality Expected: ".test{color: black;}@media (not (print)){.test{color: blue;}}.test{display: grid;}@supports (not display: grid){.test{display: block;}}" Received: ".test{color: if(not media(print): blue;else: black);}.test{display: if(not supports(display: grid): block;else: grid);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (22)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > prevent cyclic substitution context in style queries

AssertionError: expected '.test{--foo: if(style(--foo: bar): ba…' to be '.test{--foo: default;}' // Object.is equality Expected: ".test{--foo: default;}" Received: ".test{--foo: if(style(--foo: bar): baz;else: default);}" ❯ test/integrated.test.js:42:45

Check failure on line 42 in packages/css-if-polyfill/test/integrated.test.js

View workflow job for this annotation

GitHub Actions / Test (22)

test/integrated.test.js > Integrated CSS if() Polyfill > Build-time transformation > handle if() functions without else clause (empty token stream)

AssertionError: expected '.test{color: if(media(print): red);}.…' to be '.test{background: transparent;}' // Object.is equality Expected: ".test{background: transparent;}" Received: ".test{color: if(media(print): red);}.test{background: if(supports(display: none): transparent);}" ❯ test/integrated.test.js:42:45
normalizeCSS(expected)
);
// NativeCSS should be empty or contain only fallback values
expect(result.nativeCSS).toBe('');
});
}

test('handles mixed media and style conditions', () => {
const { input } = loadFixture('mixed-conditions');
const result = buildTimeTransform(input);
Expand Down
13 changes: 12 additions & 1 deletion packages/postcss-if-function/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,19 @@ function postcssIfFunction(options = {}) {
// Apply transformation
const transformed = buildTimeTransform(cssText);

// If no transformations were made, keep original
if (transformed.nativeCSS === cssText) {
// No transformations were made
return;
}

// If we have runtime rules but no native CSS (or only empty nativeCSS),
// we can't transform at build-time, so preserve the original
if (
(!transformed.nativeCSS ||
transformed.nativeCSS.trim() === '') &&
transformed.hasRuntimeRules
) {
// Keep the original CSS unchanged since these features need runtime processing
return;
}

Expand Down
13 changes: 12 additions & 1 deletion packages/postcss-if-function/test/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { describe, expect, it, vi } from 'vitest';
import {
loadFixture,
normalizeCSS,
postcssFixtureTests
postcssFixtureTests,
runtimeOnlyFixtureTests
} from '../../../test/scripts/fixture-utils.js';
import { postcssIfFunction } from '../src/index.js';

Expand All @@ -27,6 +28,16 @@ describe('postcss-if-function plugin', () => {
});
}

// Runtime-only fixtures that cannot be transformed at build time
// PostCSS should preserve these unchanged
for (const { fixture, description } of runtimeOnlyFixtureTests) {
it(`should preserve ${description} unchanged`, async () => {
const { input } = loadFixture(fixture);
// For runtime-only features, PostCSS should preserve the original CSS
await run(input, input);
});
}

it('should work with logTransformations option', async () => {
const { input, expected } = loadFixture('basic-media');

Expand Down
72 changes: 72 additions & 0 deletions scripts/generate-fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env node

/**
* Generate expected outputs for new fixtures
*
* This development utility script helps generate expected output files for new test fixtures
* by running input CSS through the polyfill transformation engine.
*
* Usage:
* 1. Create your .input.css files in test/fixtures/
* 2. Add the fixture names to the newFixtures array below
* 3. Run: node scripts/generate-fixtures.js
* 4. Review and adjust the generated .expected.css files as needed
*
* Note: Always review the generated outputs to ensure they match expected behavior.
*/

import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

// Import the transform function
import { buildTimeTransform } from '../packages/css-if-polyfill/src/index.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.join(__dirname, '..', 'test', 'fixtures');

// Configure which fixtures to process
// Add fixture names (without .input.css suffix) to generate expected outputs
const newFixtures = [
'empty-token-stream',
'cyclic-substitution',
'complex-supports',
'boolean-negation',
'multiple-mixed-conditions',
'nested-media-features'
];

async function generateExpectedOutputs() {
const results = await Promise.all(
newFixtures.map(async (fixture) => {
const inputPath = path.join(fixturesDir, `${fixture}.input.css`);
const expectedPath = path.join(
fixturesDir,
`${fixture}.expected.css`
);

try {
const input = await readFile(inputPath, 'utf8');
console.log(`Processing ${fixture}...`);
console.log('Input:', input);

const result = buildTimeTransform(input);
console.log('Output:', result);

// Extract the appropriate CSS output (nativeCSS for static transforms, runtimeCSS for dynamic)
const outputCSS = result.nativeCSS || result.runtimeCSS || '';

await writeFile(expectedPath, outputCSS);
console.log(`✓ Generated ${fixture}.expected.css\n`);
return { fixture, success: true };
} catch (error) {
console.error(`✗ Failed to process ${fixture}:`, error.message);
return { fixture, success: false, error };
}
})
);

return results;
}

await generateExpectedOutputs();
16 changes: 16 additions & 0 deletions test/fixtures/boolean-negation.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.test {
color: black;
}
@media (not (print)) {
.test {
color: blue;
}
}
.test {
display: grid;
}
@supports (not display: grid) {
Copy link
Preview

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

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

The @supports rule syntax (not display: grid) is incorrect. According to CSS @supports specification, the correct syntax should be not (display: grid) with parentheses around the condition being negated.

Copilot generated this review using guidance from copilot-instructions.md.

.test {
display: block;
}
}
4 changes: 4 additions & 0 deletions test/fixtures/boolean-negation.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.test {
color: if(not media(print): blue; else: black);
display: if(not supports(display: grid): block; else: grid);
}
21 changes: 21 additions & 0 deletions test/fixtures/complex-supports.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.test {
display: block;
}
@supports ((display: grid) and (gap: 1rem)) {
.test {
display: grid;
}
}
.test {
color: blue;
}
@supports (color: hsl(from red h s l)) {
.test {
color: hsl(from red h s l);
}
}
@supports (color: lab(50% 20 -30)) {
.test {
color: lab(50% 20 -30);
}
}
4 changes: 4 additions & 0 deletions test/fixtures/complex-supports.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.test {
display: if(supports((display: grid) and (gap: 1rem)): grid; else: block);
color: if(supports(color: lab(50% 20 -30)): lab(50% 20 -30); supports(color: hsl(from red h s l)): hsl(from red h s l); else: blue);
}
3 changes: 3 additions & 0 deletions test/fixtures/cyclic-substitution.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test {
--foo: default;
}
3 changes: 3 additions & 0 deletions test/fixtures/cyclic-substitution.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test {
--foo: if(style(--foo: bar): baz; else: default);
}
3 changes: 3 additions & 0 deletions test/fixtures/empty-token-stream.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test {
background: transparent;
}
4 changes: 4 additions & 0 deletions test/fixtures/empty-token-stream.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.test {
color: if(media(print): red);
background: if(supports(display: none): transparent);
}
18 changes: 18 additions & 0 deletions test/fixtures/multiple-mixed-conditions.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.responsive {
padding: 15px;
}
@supports (display: grid) {
.responsive {
padding: 25px;
}
}
@media (width >= 768px) {
.responsive {
padding: 30px;
}
}
@media (width >= 1200px) {
.responsive {
padding: 40px;
}
}
9 changes: 9 additions & 0 deletions test/fixtures/multiple-mixed-conditions.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.responsive {
padding: if(
media(width >= 1200px): 40px;
media(width >= 768px): 30px;
supports(display: grid): 25px;
style(--large-padding): 35px;
else: 15px
);
}
16 changes: 16 additions & 0 deletions test/fixtures/nested-media-features.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.test {
width: 100%;
}
@media ((orientation: landscape) and (hover: hover)) {
.test {
width: 80%;
}
}
.test {
font-size: 1rem;
}
@media ((min-width: 768px) and (max-width: 1024px) and (prefers-reduced-motion: no-preference)) {
.test {
font-size: 1.2rem;
}
}
4 changes: 4 additions & 0 deletions test/fixtures/nested-media-features.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.test {
width: if(media((orientation: landscape) and (hover: hover)): 80%; else: 100%);
font-size: if(media((min-width: 768px) and (max-width: 1024px) and (prefers-reduced-motion: no-preference)): 1.2rem; else: 1rem);
}
32 changes: 32 additions & 0 deletions test/scripts/fixture-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,38 @@ export const basicFixtureTests = [
{
fixture: 'no-if-functions',
description: 'preserve CSS without if() functions'
},
{
fixture: 'complex-supports',
description: 'handle complex supports conditions with multiple values'
},
{
fixture: 'nested-media-features',
description: 'handle complex nested media feature queries'
}
];

/**
* Runtime-only test configurations for features that cannot be transformed at build time
*/
export const runtimeOnlyFixtureTests = [
{
fixture: 'empty-token-stream',
description:
'handle if() functions without else clause (empty token stream)'
},
{
fixture: 'cyclic-substitution',
description: 'prevent cyclic substitution context in style queries'
},
{
fixture: 'boolean-negation',
description: 'handle boolean negation in conditions'
},
{
fixture: 'multiple-mixed-conditions',
description:
'handle multiple mixed condition types in single if() function'
}
];

Expand Down
Loading