@@ -9,19 +9,18 @@ import { SassProcessor } from '../SassProcessor';
99
1010const projectFolder: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!;
1111const fixturesFolder: string = `${projectFolder}/src/test/fixtures`;
12- const testOutputFolder: string = `${projectFolder}/temp/test-output`;
1312
14- function createProcessor(preserveIcssExports: boolean): {
13+ // Fake output folder paths — never actually written to disk because FileSystem.writeFileAsync is mocked.
14+ const CSS_OUTPUT_FOLDER: string = '/fake/output/css';
15+ const DTS_OUTPUT_FOLDER: string = '/fake/output/dts';
16+
17+ function createProcessor(
18+ terminalProvider: StringBufferTerminalProvider,
19+ preserveIcssExports: boolean
20+ ): {
1521 processor: SassProcessor;
16- dtsOutputFolder: string;
17- cssOutputFolder: string;
1822 logger: MockScopedLogger;
1923} {
20- const suffix: string = preserveIcssExports ? 'preserve' : 'strip';
21- const dtsOutputFolder: string = `${testOutputFolder}/${suffix}/dts`;
22- const cssOutputFolder: string = `${testOutputFolder}/${suffix}/css`;
23-
24- const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(false);
2524 const terminal: Terminal = new Terminal(terminalProvider);
2625 const logger: MockScopedLogger = new MockScopedLogger(terminal);
2726
@@ -30,91 +29,115 @@ function createProcessor(preserveIcssExports: boolean): {
3029 buildFolder: projectFolder,
3130 concurrency: 1,
3231 srcFolder: fixturesFolder,
33- dtsOutputFolders: [dtsOutputFolder ],
34- cssOutputFolders: [{ folder: cssOutputFolder , shimModuleFormat: undefined }],
32+ dtsOutputFolders: [DTS_OUTPUT_FOLDER ],
33+ cssOutputFolders: [{ folder: CSS_OUTPUT_FOLDER , shimModuleFormat: undefined }],
3534 exportAsDefault: true,
3635 preserveIcssExports
3736 });
3837
39- return { processor, dtsOutputFolder, cssOutputFolder, logger };
38+ return { processor, logger };
4039}
4140
4241async function compileFixtureAsync(processor: SassProcessor, fixtureFilename: string): Promise<void> {
43- const absolutePath: string = `${fixturesFolder}/${fixtureFilename}`;
44- await processor.compileFilesAsync(new Set([absolutePath]));
42+ await processor.compileFilesAsync(new Set([`${fixturesFolder}/${fixtureFilename}`]));
4543}
4644
47- async function readCssOutputAsync(cssOutputFolder: string, fixtureFilename: string): Promise<string> {
48- // Strip last extension (.scss/.sass), append .css
49- const withoutExt: string = fixtureFilename.slice(0, fixtureFilename.lastIndexOf('.'));
50- return await FileSystem.readFileAsync(`${cssOutputFolder}/${withoutExt}.css`);
51- }
45+ describe(SassProcessor.name, () => {
46+ let terminalProvider: StringBufferTerminalProvider;
47+ /** Files captured by the mocked FileSystem.writeFileAsync, keyed by absolute path. */
48+ let writtenFiles: Map<string, string>;
49+
50+ /** Returns the content written to a path whose last segment matches the given filename. */
51+ function getWrittenFile(filename: string): string {
52+ for (const [filePath, content] of writtenFiles) {
53+ if (filePath.endsWith(`/${filename}`)) {
54+ return content;
55+ }
56+ }
5257
53- async function readDtsOutputAsync(dtsOutputFolder: string, fixtureFilename: string): Promise<string> {
54- return await FileSystem.readFileAsync(`${dtsOutputFolder}/${fixtureFilename}.d.ts`);
55- }
58+ throw new Error(
59+ `No file written matching ".../${filename}". Written paths:\n${[...writtenFiles.keys()].join('\n')}`
60+ );
61+ }
5662
57- describe(SassProcessor.name, () => {
58- beforeEach(async () => {
59- await FileSystem.ensureEmptyFolderAsync(testOutputFolder);
63+ function getCssOutput(fixtureFilename: string): string {
64+ // SassProcessor strips the last extension then appends .css
65+ // export-only.module.scss → export-only.module.css
66+ const withoutExt: string = fixtureFilename.slice(0, fixtureFilename.lastIndexOf('.'));
67+ return getWrittenFile(`${withoutExt}.css`);
68+ }
69+
70+ function getDtsOutput(fixtureFilename: string): string {
71+ return getWrittenFile(`${fixtureFilename}.d.ts`);
72+ }
73+
74+ beforeEach(() => {
75+ terminalProvider = new StringBufferTerminalProvider();
76+
77+ writtenFiles = new Map();
78+ jest.spyOn(FileSystem, 'writeFileAsync').mockImplementation(async (filePath, content) => {
79+ writtenFiles.set(filePath as string, content as string);
80+ });
81+ });
82+
83+ afterEach(() => {
84+ jest.restoreAllMocks();
85+
86+ expect(writtenFiles).toMatchSnapshot('written-files');
87+ expect(terminalProvider.getAllOutputAsChunks({ asLines: true })).toMatchSnapshot('terminal-output');
6088 });
6189
6290 describe('export-only.module.scss', () => {
6391 it('strips the :export block from CSS when preserveIcssExports is false', async () => {
64- const { processor, cssOutputFolder } = createProcessor(false);
92+ const { processor } = createProcessor(terminalProvider, false);
6593 await compileFixtureAsync(processor, 'export-only.module.scss');
66- const css: string = await readCssOutputAsync(cssOutputFolder, 'export-only.module.scss');
67- expect(css).toMatchSnapshot();
94+ const css: string = getCssOutput('export-only.module.scss');
6895 expect(css).not.toContain(':export');
6996 });
7097
7198 it('preserves the :export block in CSS when preserveIcssExports is true', async () => {
72- const { processor, cssOutputFolder } = createProcessor(true);
99+ const { processor } = createProcessor(terminalProvider, true);
73100 await compileFixtureAsync(processor, 'export-only.module.scss');
74- const css: string = await readCssOutputAsync(cssOutputFolder, 'export-only.module.scss');
75- expect(css).toMatchSnapshot();
101+ const css: string = getCssOutput('export-only.module.scss');
76102 expect(css).toContain(':export');
77103 });
78104
79105 it('generates the same .d.ts regardless of preserveIcssExports', async () => {
80- const { processor: processorFalse, dtsOutputFolder: dtsFalseFolder } = createProcessor(false);
81- const { processor: processorTrue, dtsOutputFolder: dtsTrueFolder } = createProcessor(true);
82-
106+ const { processor: processorFalse } = createProcessor(terminalProvider, false);
83107 await compileFixtureAsync(processorFalse, 'export-only.module.scss');
84- await compileFixtureAsync(processorTrue, 'export-only.module.scss');
108+ const dtsFalse: string = getDtsOutput('export-only.module.scss');
109+
110+ writtenFiles.clear();
85111
86- const dtsFalse: string = await readDtsOutputAsync(dtsFalseFolder, 'export-only.module.scss');
87- const dtsTrue: string = await readDtsOutputAsync(dtsTrueFolder, 'export-only.module.scss');
112+ const { processor: processorTrue } = createProcessor(terminalProvider, true);
113+ await compileFixtureAsync(processorTrue, 'export-only.module.scss');
114+ const dtsTrue: string = getDtsOutput('export-only.module.scss');
88115
89- expect(dtsFalse).toMatchSnapshot();
90116 expect(dtsFalse).toEqual(dtsTrue);
91117 });
92118 });
93119
94120 describe('classes-and-exports.module.scss', () => {
95121 it('strips the :export block from CSS when preserveIcssExports is false', async () => {
96- const { processor, cssOutputFolder } = createProcessor(false);
122+ const { processor } = createProcessor(terminalProvider, false);
97123 await compileFixtureAsync(processor, 'classes-and-exports.module.scss');
98- const css: string = await readCssOutputAsync(cssOutputFolder, 'classes-and-exports.module.scss');
99- expect(css).toMatchSnapshot();
124+ const css: string = getCssOutput('classes-and-exports.module.scss');
100125 expect(css).not.toContain(':export');
101126 expect(css).toContain('.root');
102127 });
103128
104129 it('preserves the :export block in CSS when preserveIcssExports is true', async () => {
105- const { processor, cssOutputFolder } = createProcessor(true);
130+ const { processor } = createProcessor(terminalProvider, true);
106131 await compileFixtureAsync(processor, 'classes-and-exports.module.scss');
107- const css: string = await readCssOutputAsync(cssOutputFolder, 'classes-and-exports.module.scss');
108- expect(css).toMatchSnapshot();
132+ const css: string = getCssOutput('classes-and-exports.module.scss');
109133 expect(css).toContain(':export');
110134 expect(css).toContain('.root');
111135 });
112136
113137 it('generates correct .d.ts with both class names and :export values', async () => {
114- const { processor, dtsOutputFolder } = createProcessor(false);
138+ const { processor } = createProcessor(terminalProvider, false);
115139 await compileFixtureAsync(processor, 'classes-and-exports.module.scss');
116- const dts: string = await readDtsOutputAsync(dtsOutputFolder, 'classes-and-exports.module.scss');
117- expect(dts).toMatchSnapshot();
140+ const dts: string = getDtsOutput('classes-and-exports.module.scss');
118141 expect(dts).toContain('root');
119142 expect(dts).toContain('highlighted');
120143 expect(dts).toContain('themeColor');
@@ -124,10 +147,9 @@ describe(SassProcessor.name, () => {
124147
125148 describe('sass-variables-and-exports.module.scss (Sass variables, nesting, BEM)', () => {
126149 it('resolves Sass variables and expands nested rules in CSS output', async () => {
127- const { processor, cssOutputFolder } = createProcessor(false);
150+ const { processor } = createProcessor(terminalProvider, false);
128151 await compileFixtureAsync(processor, 'sass-variables-and-exports.module.scss');
129- const css: string = await readCssOutputAsync(cssOutputFolder, 'sass-variables-and-exports.module.scss');
130- expect(css).toMatchSnapshot();
152+ const css: string = getCssOutput('sass-variables-and-exports.module.scss');
131153 // Sass variables should be resolved to literal values
132154 expect(css).toContain('#0078d4');
133155 expect(css).toContain('#106ebe');
@@ -139,21 +161,19 @@ describe(SassProcessor.name, () => {
139161 });
140162
141163 it('resolves Sass variables inside the :export block when preserveIcssExports is true', async () => {
142- const { processor, cssOutputFolder } = createProcessor(true);
164+ const { processor } = createProcessor(terminalProvider, true);
143165 await compileFixtureAsync(processor, 'sass-variables-and-exports.module.scss');
144- const css: string = await readCssOutputAsync(cssOutputFolder, 'sass-variables-and-exports.module.scss');
145- expect(css).toMatchSnapshot();
146- // The :export block should contain the resolved variable values, not the variable names
166+ const css: string = getCssOutput('sass-variables-and-exports.module.scss');
167+ // The :export block should contain resolved values, not Sass variable names
147168 expect(css).toContain(':export');
148169 expect(css).toContain('#0078d4');
149170 expect(css).not.toContain('$primary-color');
150171 });
151172
152173 it('generates .d.ts with resolved :export keys as typed properties', async () => {
153- const { processor, dtsOutputFolder } = createProcessor(false);
174+ const { processor } = createProcessor(terminalProvider, false);
154175 await compileFixtureAsync(processor, 'sass-variables-and-exports.module.scss');
155- const dts: string = await readDtsOutputAsync(dtsOutputFolder, 'sass-variables-and-exports.module.scss');
156- expect(dts).toMatchSnapshot();
176+ const dts: string = getDtsOutput('sass-variables-and-exports.module.scss');
157177 expect(dts).toContain('container');
158178 expect(dts).toContain('primaryColor');
159179 expect(dts).toContain('secondaryColor');
@@ -163,10 +183,9 @@ describe(SassProcessor.name, () => {
163183
164184 describe('mixin-with-exports.module.scss (Sass @mixin)', () => {
165185 it('expands @mixin calls in CSS output', async () => {
166- const { processor, cssOutputFolder } = createProcessor(false);
186+ const { processor } = createProcessor(terminalProvider, false);
167187 await compileFixtureAsync(processor, 'mixin-with-exports.module.scss');
168- const css: string = await readCssOutputAsync(cssOutputFolder, 'mixin-with-exports.module.scss');
169- expect(css).toMatchSnapshot();
188+ const css: string = getCssOutput('mixin-with-exports.module.scss');
170189 // Mixin output should be inlined — no @mixin or @include in the output
171190 expect(css).not.toContain('@mixin');
172191 expect(css).not.toContain('@include');
@@ -176,20 +195,18 @@ describe(SassProcessor.name, () => {
176195 });
177196
178197 it('preserves :export alongside expanded @mixin output when preserveIcssExports is true', async () => {
179- const { processor, cssOutputFolder } = createProcessor(true);
198+ const { processor } = createProcessor(terminalProvider, true);
180199 await compileFixtureAsync(processor, 'mixin-with-exports.module.scss');
181- const css: string = await readCssOutputAsync(cssOutputFolder, 'mixin-with-exports.module.scss');
182- expect(css).toMatchSnapshot();
200+ const css: string = getCssOutput('mixin-with-exports.module.scss');
183201 expect(css).toContain(':export');
184202 expect(css).toContain('display: flex');
185203 expect(css).not.toContain('@mixin');
186204 });
187205
188206 it('generates .d.ts with :export values and class names from @mixin-using file', async () => {
189- const { processor, dtsOutputFolder } = createProcessor(false);
207+ const { processor } = createProcessor(terminalProvider, false);
190208 await compileFixtureAsync(processor, 'mixin-with-exports.module.scss');
191- const dts: string = await readDtsOutputAsync(dtsOutputFolder, 'mixin-with-exports.module.scss');
192- expect(dts).toMatchSnapshot();
209+ const dts: string = getDtsOutput('mixin-with-exports.module.scss');
193210 expect(dts).toContain('card');
194211 expect(dts).toContain('cardRadius');
195212 expect(dts).toContain('animationDuration');
@@ -198,33 +215,29 @@ describe(SassProcessor.name, () => {
198215
199216 describe('extend-with-exports.module.scss (Sass @extend / placeholder selectors)', () => {
200217 it('merges @extend selectors and strips :export when preserveIcssExports is false', async () => {
201- const { processor, cssOutputFolder } = createProcessor(false);
218+ const { processor } = createProcessor(terminalProvider, false);
202219 await compileFixtureAsync(processor, 'extend-with-exports.module.scss');
203- const css: string = await readCssOutputAsync(cssOutputFolder, 'extend-with-exports.module.scss');
204- expect(css).toMatchSnapshot();
205- // Placeholder %button-base should not appear literally; its rules should be merged into the
206- // selectors that @extend it
220+ const css: string = getCssOutput('extend-with-exports.module.scss');
221+ // Placeholder %button-base should not appear literally; its rules should be merged
207222 expect(css).not.toContain('%button-base');
208223 expect(css).toContain('.primaryButton');
209224 expect(css).toContain('.dangerButton');
210225 expect(css).not.toContain(':export');
211226 });
212227
213228 it('preserves :export alongside @extend-merged output when preserveIcssExports is true', async () => {
214- const { processor, cssOutputFolder } = createProcessor(true);
229+ const { processor } = createProcessor(terminalProvider, true);
215230 await compileFixtureAsync(processor, 'extend-with-exports.module.scss');
216- const css: string = await readCssOutputAsync(cssOutputFolder, 'extend-with-exports.module.scss');
217- expect(css).toMatchSnapshot();
231+ const css: string = getCssOutput('extend-with-exports.module.scss');
218232 expect(css).toContain(':export');
219233 expect(css).toContain('.primaryButton');
220234 expect(css).not.toContain('%button-base');
221235 });
222236
223237 it('generates .d.ts with class names and :export values for @extend file', async () => {
224- const { processor, dtsOutputFolder } = createProcessor(false);
238+ const { processor } = createProcessor(terminalProvider, false);
225239 await compileFixtureAsync(processor, 'extend-with-exports.module.scss');
226- const dts: string = await readDtsOutputAsync(dtsOutputFolder, 'extend-with-exports.module.scss');
227- expect(dts).toMatchSnapshot();
240+ const dts: string = getDtsOutput('extend-with-exports.module.scss');
228241 expect(dts).toContain('primaryButton');
229242 expect(dts).toContain('dangerButton');
230243 expect(dts).toContain('colorPrimary');
@@ -234,17 +247,8 @@ describe(SassProcessor.name, () => {
234247
235248 describe('error reporting', () => {
236249 it('emits an error for invalid SCSS syntax', async () => {
237- // Write a temporary invalid fixture to disk, compile it, then clean up.
238- const invalidFixturePath: string = `${fixturesFolder}/invalid.module.scss`;
239- await FileSystem.writeFileAsync(invalidFixturePath, '.broken { color: ; }');
240-
241- const { processor, logger } = createProcessor(false);
242- try {
243- await processor.compileFilesAsync(new Set([invalidFixturePath]));
244- } finally {
245- await FileSystem.deleteFileAsync(invalidFixturePath);
246- }
247-
250+ const { processor, logger } = createProcessor(terminalProvider, false);
251+ await compileFixtureAsync(processor, 'invalid.module.scss');
248252 expect(logger.errors.length).toBeGreaterThan(0);
249253 });
250254 });
0 commit comments