Add Sass tests (#1919)

This commit is contained in:
Drew Powers 2021-11-22 14:24:42 -07:00 committed by GitHub
parent 1304518b74
commit d3476f24d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 483 additions and 200 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Bump Sass dependency version

View file

@ -90,7 +90,7 @@
"rehype-slug": "^5.0.0",
"resolve": "^1.20.0",
"rollup": "^2.57.0",
"sass": "^1.43.3",
"sass": "^1.43.4",
"semver": "^7.3.5",
"send": "^0.17.1",
"shiki": "^0.9.10",

View file

@ -0,0 +1,269 @@
/**
* CSS test
* Run this test first! This uses quite a bit of memory, so prefixing with `0-` helps it start and finish early,
* rather than trying to start up when all other threads are busy and having to fight for resources
*/
import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Styles SSR', function () {
this.timeout(30000); // test needs a little more time in CI
let fixture;
let index$;
let bundledCSS;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/0-css/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue'],
});
await fixture.build();
// get bundled CSS (will be hashed, hence DOM query)
const html = await fixture.readFile('/index.html');
index$ = cheerio.load(html);
const bundledCSSHREF = index$('link[rel=stylesheet][href^=assets/]').attr('href');
bundledCSS = await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/'));
});
describe('Astro styles', () => {
it('HTML and CSS scoped correctly', async () => {
const $ = index$;
const el1 = $('#dynamic-class');
const el2 = $('#dynamic-vis');
const classes = $('#class').attr('class').split(' ');
const scopedClass = classes.find((name) => /^astro-[A-Za-z0-9-]+/.test(name));
// 1. check HTML
expect(el1.attr('class')).to.equal(`blue ${scopedClass}`);
expect(el2.attr('class')).to.equal(`visible ${scopedClass}`);
// 2. check CSS
expect(bundledCSS).to.include(`.blue.${scopedClass}{color:#b0e0e6}.color\\:blue.${scopedClass}{color:#b0e0e6}.visible.${scopedClass}{display:block}`);
});
it('No <style> skips scoping', async () => {
const $ = index$;
// Astro component without <style> should not include scoped class
expect($('#no-scope').attr('class')).to.equal(undefined);
});
it('Child inheritance', async () => {
const $ = index$;
expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/);
});
it('Using hydrated components adds astro-root styles', async () => {
expect(bundledCSS).to.include('display:contents');
});
it('<style lang="sass">', async () => {
expect(bundledCSS).to.match(new RegExp('h1.astro-[^{]*{color:#90ee90}'));
});
it('<style lang="scss">', async () => {
expect(bundledCSS).to.match(new RegExp('h1.astro-[^{]*{color:#ff69b4}'));
});
});
describe('Styles in src/', () => {
it('.css', async () => {
expect(bundledCSS).to.match(new RegExp('.linked-css[^{]*{color:gold'));
});
it('.sass', async () => {
expect(bundledCSS).to.match(new RegExp('.linked-sass[^{]*{color:#789'));
});
it('.scss', async () => {
expect(bundledCSS).to.match(new RegExp('.linked-scss[^{]*{color:#6b8e23'));
});
});
describe('JSX', () => {
it('.css', async () => {
const $ = index$;
const el = $('#react-css');
// 1. check HTML
expect(el.attr('class')).to.include('react-title');
// 2. check CSS
expect(bundledCSS).to.include('.react-title{');
});
it('.module.css', async () => {
const $ = index$;
const el = $('#react-module-css');
const classes = el.attr('class').split(' ');
const moduleClass = classes.find((name) => /^_title_[A-Za-z0-9-_]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include(moduleClass);
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.${moduleClass}[^{]*{font-family:fantasy}`));
});
it('.sass', async () => {
const $ = index$;
const el = $('#react-sass');
// 1. check HTML
expect(el.attr('class')).to.include('react-sass-title');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.react-sass-title[^{]*{font-family:fantasy}`));
});
it('.scss', async () => {
const $ = index$;
const el = $('#react-scss');
// 1. check HTML
expect(el.attr('class')).to.include('react-scss-title');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.react-scss-title[^{]*{font-family:fantasy}`));
});
it('.module.sass', async () => {
const $ = index$;
const el = $('#react-module-sass');
const classes = el.attr('class').split(' ');
const moduleClass = classes.find((name) => /^_title_[A-Za-z0-9-_]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include(moduleClass);
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.${moduleClass}[^{]*{font-family:fantasy}`));
});
it('.module.scss', async () => {
const $ = index$;
const el = $('#react-module-scss');
const classes = el.attr('class').split(' ');
const moduleClass = classes.find((name) => /^_title_[A-Za-z0-9-_]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include(moduleClass);
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.${moduleClass}[^{]*{font-family:fantasy}`));
});
});
describe('Vue', () => {
it('<style>', async () => {
const $ = index$;
const el = $('#vue-css');
// 1. check HTML
expect(el.attr('class')).to.include('vue-css');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.vue-css[^{]*{font-family:cursive`));
});
it('<style scoped>', async () => {
const $ = index$;
const el = $('#vue-scoped');
// find data-v-* attribute (how Vue CSS scoping works)
const { attribs } = el.get(0);
const scopeId = Object.keys(attribs).find((k) => k.startsWith('data-v-'));
expect(scopeId).to.be.ok;
// 1. check HTML
expect(el.attr('class')).to.include('vue-scoped');
// 2. check CSS
expect(bundledCSS).to.include(`.vue-scoped[${scopeId}]`);
});
it('<style module>', async () => {
const $ = index$;
const el = $('#vue-modules');
const classes = el.attr('class').split(' ');
const moduleClass = classes.find((name) => /^_title_[A-Za-z0-9-_]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include(moduleClass);
// 2. check CSS
expect(bundledCSS).to.include(`${moduleClass}{`);
});
it('<style lang="sass">', async () => {
const $ = index$;
const el = $('#vue-sass');
// 1. check HTML
expect(el.attr('class')).to.include('vue-sass');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.vue-sass[^{]*{font-family:cursive`));
});
it('<style lang="scss">', async () => {
const $ = index$;
const el = $('#vue-scss');
// 1. check HTML
expect(el.attr('class')).to.include('vue-scss');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.vue-scss[^{]*{font-family:cursive`));
});
});
describe('Svelte', () => {
it('<style>', async () => {
const $ = index$;
const el = $('#svelte-css');
const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => /^s-[A-Za-z0-9-]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include('svelte-css');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.svelte-css.${scopedClass}[^{]*{font-family:"Comic Sans MS"`));
});
// TODO: fix Sass in Svelte
it.skip('<style lang="sass">', async () => {
const $ = index$;
const el = $('#svelte-sass');
const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => /^s-[A-Za-z0-9-]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include('svelte-sass');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.svelte-sass.${scopedClass}[^{]*{font-family:"Comic Sans MS"`));
});
// TODO: fix Sass in Svelte
it.skip('<style lang="scss">', async () => {
const $ = index$;
const el = $('#svelte-scss');
const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => /^s-[A-Za-z0-9-]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include('svelte-scss');
// 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.svelte-scss.${scopedClass}[^{]*{font-family:"Comic Sans MS"`));
});
});
});

View file

@ -1,141 +0,0 @@
import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Styles SSR', function () {
let fixture;
let index$;
let bundledCSS;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-styles-ssr/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue'],
});
await fixture.build();
// get bundled CSS (will be hashed, hence DOM query)
const html = await fixture.readFile('/index.html');
index$ = cheerio.load(html);
const bundledCSSHREF = index$('link[rel=stylesheet][href^=assets/]').attr('href');
bundledCSS = await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/'));
});
describe('Astro styles', () => {
it('HTML and CSS scoped correctly', async () => {
const $ = index$;
const el1 = $('#dynamic-class');
const el2 = $('#dynamic-vis');
const classes = $('#class').attr('class').split(' ');
const scopedClass = classes.find((name) => /^astro-[A-Za-z0-9-]+/.test(name));
// 1. check HTML
expect(el1.attr('class')).to.equal(`blue ${scopedClass}`);
expect(el2.attr('class')).to.equal(`visible ${scopedClass}`);
// 2. check CSS
expect(bundledCSS).to.include(`.blue.${scopedClass}{color:#b0e0e6}.color\\:blue.${scopedClass}{color:#b0e0e6}.visible.${scopedClass}{display:block}`);
});
it('No <style> skips scoping', async () => {
const $ = index$;
// Astro component without <style> should not include scoped class
expect($('#no-scope').attr('class')).to.equal(undefined);
});
it('Child inheritance', async () => {
const $ = index$;
expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/);
});
it('Using hydrated components adds astro-root styles', async () => {
expect(bundledCSS).to.include('display:contents');
});
});
describe('JSX', () => {
it('CSS', async () => {
const $ = index$;
const el = $('#react-css');
// 1. check HTML
expect(el.attr('class')).to.include('react-title');
// 2. check CSS
expect(bundledCSS).to.include('.react-title{');
});
it('CSS Modules', async () => {
const $ = index$;
const el = $('#react-modules');
const classes = el.attr('class').split(' ');
const moduleClass = classes.find((name) => /^_title_[A-Za-z0-9-_]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include(moduleClass);
// 2. check CSS
expect(bundledCSS).to.include(`.${moduleClass}{`);
});
});
describe('Vue', () => {
it('CSS', async () => {
const $ = index$;
const el = $('#vue-css');
// 1. check HTML
expect(el.attr('class')).to.include('vue-title');
// 2. check CSS
expect(bundledCSS).to.include('.vue-title{');
});
it('Scoped styles', async () => {
const $ = index$;
const el = $('#vue-scoped');
// find data-v-* attribute (how Vue CSS scoping works)
const { attribs } = el.get(0);
const scopeId = Object.keys(attribs).find((k) => k.startsWith('data-v-'));
expect(scopeId).to.be.ok;
// 1. check HTML
expect(el.attr('class')).to.include('vue-scoped');
// 2. check CSS
expect(bundledCSS).to.include(`.vue-scoped[${scopeId}]`);
});
it('CSS Modules', async () => {
const $ = index$;
const el = $('#vue-modules');
const classes = el.attr('class').split(' ');
const moduleClass = classes.find((name) => /^_title_[A-Za-z0-9-_]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include(moduleClass);
// 2. check CSS
expect(bundledCSS).to.include(`${moduleClass}{`);
});
});
describe('Svelte', () => {
it('Scoped styles', async () => {
const $ = index$;
const el = $('#svelte-scoped');
const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => /^s-[A-Za-z0-9-]+/.test(name));
// 1. check HTML
expect(el.attr('class')).to.include('svelte-title');
// 2. check CSS
expect(bundledCSS).to.include(`.svelte-title.${scopedClass}`);
});
});
});

View file

@ -0,0 +1,8 @@
<style lang="sass">
$color: #90ee90
h1
color: $color
</style>
<h1>Color</h1>

View file

@ -0,0 +1,9 @@
<style lang="scss">
$color: #ff69b4;
h1 {
color: $color;
}
</style>
<h1>Color</h1>

View file

@ -3,7 +3,7 @@ import Styles from './ReactModules.module.css';
function ReactModules() {
return (
<h1 id="react-modules" className={Styles.title}>
<h1 id="react-module-css" className={Styles.title}>
React Modules
</h1>
);

View file

@ -0,0 +1,4 @@
$font-family: fantasy
.title
font-family: $font-family

View file

@ -0,0 +1,5 @@
$font-family: fantasy;
.title {
font-family: $font-family;
}

View file

@ -0,0 +1,11 @@
import React from 'react';
import Styles from './ReactModules.module.sass';
function ReactModules() {
return (
<h1 id="react-module-sass" className={Styles.title}>
React Modules
</h1>
);
}
export default ReactModules;

View file

@ -0,0 +1,11 @@
import React from 'react';
import Styles from './ReactModules.module.scss';
function ReactModules() {
return (
<h1 id="react-module-scss" className={Styles.title}>
React Modules
</h1>
);
}
export default ReactModules;

View file

@ -0,0 +1,11 @@
import React from 'react';
import './ReactSass.sass';
function ReactCSS() {
return (
<h1 id="react-sass" className="react-sass-title">
React Global Sass
</h1>
);
}
export default ReactCSS;

View file

@ -0,0 +1,5 @@
$font-family: fantasy
.react-sass-title
font-family: $font-family

View file

@ -0,0 +1,11 @@
import React from 'react';
import './ReactScss.scss';
function ReactCSS() {
return (
<h1 id="react-scss" className="react-scss-title">
React Global Scss
</h1>
);
}
export default ReactCSS;

View file

@ -0,0 +1,6 @@
$font-family: fantasy;
.react-scss-title {
font-family: $font-family;
}

View file

@ -0,0 +1,7 @@
<h1 id="svelte-css" class="svelte-css">Svelte Scoped CSS</h1>
<style>
.svelte-css {
font-family: 'Comic Sans MS', sans-serif;
}
</style>

View file

@ -0,0 +1,8 @@
<h1 id="svelte-sass" class="svelte-sass">Svelte Scoped Sass</h1>
<style lang="sass">
$font-family: 'Comic Sans MS', sans-serif
.svelte-sass
font-family: $font-family
</style>

View file

@ -0,0 +1,9 @@
<h1 id="svelte-scss" class="svelte-scss">Svelte Scoped Scss</h1>
<style lang="scss">
$font-family: 'Comic Sans MS', sans-serif;
.svelte-scss {
font-family: $font-family;
}
</style>

View file

@ -0,0 +1,9 @@
<style>
.vue-css {
font-family: cursive;
}
</style>
<template>
<h1 id="vue-css" class="vue-css">Vue Global CSS</h1>
</template>

View file

@ -0,0 +1,10 @@
<style lang="sass">
$font-family: cursive
.vue-sass
font-family: $font-family
</style>
<template>
<h1 id="vue-sass" class="vue-sass">Vue Sass</h1>
</template>

View file

@ -0,0 +1,11 @@
<style lang="scss">
$font-family: cursive;
.vue-scss {
font-family: $font-family;
}
</style>
<template>
<h1 id="vue-scss" class="vue-scss">Vue Sass</h1>
</template>

View file

@ -0,0 +1,63 @@
---
import AstroComponent from '../components/Astro.astro';
import AstroComponentNone from '../components/AstroNone.astro';
import AstroSass from '../components/AstroSass.astro';
import AstroScss from '../components/AstroScss.astro';
import ReactCSS from '../components/ReactCSS.jsx';
import ReactModules from '../components/ReactModules.jsx';
import ReactModulesSass from '../components/ReactModulesSass.jsx';
import ReactModulesScss from '../components/ReactModulesScss.jsx';
import ReactSass from '../components/ReactSass.jsx';
import ReactScss from '../components/ReactScss.jsx';
import VueCSS from '../components/VueCSS.vue';
import VueModules from '../components/VueModules.vue';
import VueSass from '../components/VueSass.vue';
import VueScoped from '../components/VueScoped.vue';
import VueScss from '../components/VueScss.vue';
import SvelteCSS from '../components/SvelteCSS.svelte';
// import SvelteSass from '../components/SvelteSass.svelte';
// import SvelteScss from '../components/SvelteScss.svelte';
import ReactDynamic from '../components/ReactDynamic.jsx';
---
<html>
<head>
<meta charset="UTF-8" />
<style lang="scss">
.wrapper {
margin-left: auto;
margin-right: auto;
max-width: 1200px;
}
.outer {
color: red;
}
</style>
<link rel="stylesheet" type="text/css" href={Astro.resolve('../styles/linked.css')}>
<link rel="stylesheet" type="text/css" href={Astro.resolve('../styles/linked.scss')}>
<link rel="stylesheet" type="text/css" href={Astro.resolve('../styles/linked.sass')}>
</head>
<body>
<div class="wrapper">
<AstroComponent class="outer" />
<AstroComponentNone />
<AstroSass />
<AstroScss />
<ReactCSS />
<ReactModules />
<ReactModulesSass />
<ReactModulesScss />
<ReactSass />
<ReactScss />
<VueCSS />
<VueModules />
<VueSass />
<VueScoped />
<VueScss />
<SvelteCSS />
<!-- <SvelteSass />
<SvelteScss /> -->
<ReactDynamic client:load />
</div>
</body>
</html>

View file

@ -0,0 +1,3 @@
.linked-css {
color: gold;
}

View file

@ -0,0 +1,2 @@
.linked-sass
color: #778899

View file

@ -0,0 +1,3 @@
.linked-scss {
color: #6b8e23;
}

View file

@ -1,7 +0,0 @@
<h1 id="svelte-scoped" class="svelte-title">Svelte Scoped CSS</h1>
<style lang="scss">
.svelte-title {
font-family: 'Comic Sans MS', sans-serif;
}
</style>

View file

@ -1,9 +0,0 @@
<style>
.vue-title {
font-family: cursive;
}
</style>
<template>
<h1 id="vue-css" class="vue-title">Vue Global CSS</h1>
</template>

View file

@ -1,40 +0,0 @@
---
import AstroComponent from '../components/Astro.astro';
import AstroComponentNone from '../components/AstroNone.astro';
import ReactCSS from '../components/ReactCSS.jsx';
import ReactModules from '../components/ReactModules.jsx';
import VueCSS from '../components/VueCSS.vue';
import VueScoped from '../components/VueScoped.vue';
import VueModules from '../components/VueModules.vue';
import SvelteScoped from '../components/SvelteScoped.svelte';
import ReactDynamic from '../components/ReactDynamic.jsx';
---
<html>
<head>
<meta charset="UTF-8" />
<style lang="scss">
.wrapper {
margin-left: auto;
margin-right: auto;
max-width: 1200px;
}
.outer {
color: red;
}
</style>
</head>
<body>
<div class="wrapper">
<AstroComponent class="outer" />
<AstroComponentNone />
<ReactCSS />
<ReactModules />
<VueCSS />
<VueScoped />
<VueModules />
<SvelteScoped />
<ReactDynamic client:load />
</div>
</body>
</html>

View file

@ -8906,7 +8906,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1,
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.43.3:
sass@^1.43.4:
version "1.43.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f"
integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==