-
Notifications
You must be signed in to change notification settings - Fork 88
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
[Shared Resources] Add JS API tests #1954
Changes from 14 commits
b71d78f
e955667
6145b4e
d1fe650
89ef7e5
95fd558
032e045
36db21b
7c17ae5
386167f
2ac64d2
a2e6f0c
25562de
286e7fa
27dbeab
6dcdd38
0a14b6f
0e3b3d9
76e676a
2730112
b1fec2d
60fc0f9
b3f1e67
6962fcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Copyright 2023 Google Inc. Use of this source code is governed by an | ||
// MIT-style license that can be found in the LICENSE file or at | ||
// https://opensource.org/licenses/MIT. | ||
|
||
import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; | ||
import {initAsyncCompiler, initCompiler} from 'sass'; | ||
|
||
import { | ||
asyncImporters, | ||
functions, | ||
getLogger, | ||
getSlowImporter, | ||
importers, | ||
} from './compiler.test'; | ||
import {sandbox} from './sandbox'; | ||
import {URL} from './utils'; | ||
|
||
describe('Compiler', () => { | ||
let compiler: Compiler; | ||
|
||
beforeEach(() => { | ||
compiler = initCompiler(); | ||
}); | ||
|
||
afterEach(() => { | ||
compiler.dispose(); | ||
}); | ||
|
||
describe('compile', () => { | ||
it('performs complete compilations', () => | ||
sandbox(dir => { | ||
const logger = getLogger(); | ||
dir.write({'input.scss': '@import "bar"; .fn {value: foo(bar)}'}); | ||
const result = compiler.compile(dir('input.scss'), { | ||
importers, | ||
functions, | ||
logger, | ||
}); | ||
expect(result.css).toEqualIgnoringWhitespace( | ||
'.import {value: bar;} .fn {value: "bar";}' | ||
); | ||
expect(logger.debug).toHaveBeenCalledTimes(1); | ||
})); | ||
|
||
it('performs compilations in callbacks', () => | ||
sandbox(dir => { | ||
dir.write({'input-nested.scss': 'x {y: z}'}); | ||
const nestedImporter = { | ||
canonicalize: () => new URL('foo:bar'), | ||
load: () => ({ | ||
contents: compiler.compile(dir('input-nested.scss')).css, | ||
syntax: 'scss' as const, | ||
}), | ||
}; | ||
dir.write({'input.scss': '@import "nested"; a {b: c}'}); | ||
const result = compiler.compile(dir('input.scss'), { | ||
importers: [nestedImporter], | ||
}); | ||
expect(result.css).toEqualIgnoringWhitespace('x {y: z;} a {b: c;}'); | ||
})); | ||
|
||
it('throws after being disposed', () => | ||
sandbox(dir => { | ||
dir.write({'input.scss': '$a: b; c {d: $a}'}); | ||
compiler.dispose(); | ||
expect(() => compiler.compile(dir('input.scss'))).toThrowError(); | ||
})); | ||
}); | ||
}); | ||
|
||
describe('AsyncCompiler', () => { | ||
let compiler: AsyncCompiler; | ||
const runs = 1000; // Number of concurrent compilations to run | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declare this in the test where it's used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved in 0a14b6f |
||
|
||
beforeEach(async () => { | ||
compiler = await initAsyncCompiler(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await compiler.dispose(); | ||
}); | ||
|
||
describe('compileAsync', () => { | ||
it( | ||
'handles multiple concurrent compilations', | ||
() => | ||
sandbox(async dir => { | ||
const logger = getLogger(); | ||
const compilations = Array(runs) | ||
.fill(0) | ||
.map((_, i) => { | ||
const filename = `input-${i}.scss`; | ||
dir.write({ | ||
[filename]: `@import "${i}"; .fn {value: foo(${i})}`, | ||
}); | ||
return compiler.compileAsync(dir(filename), { | ||
importers: asyncImporters, | ||
functions, | ||
logger, | ||
}); | ||
}); | ||
Array.from(await Promise.all(compilations)) | ||
.map((result: CompileResult) => result.css) | ||
.forEach((result, i) => { | ||
expect(result).toEqualIgnoringWhitespace( | ||
`.import {value: ${i};} .fn {value: "${i}";}` | ||
); | ||
}); | ||
expect(logger.debug).toHaveBeenCalledTimes(runs); | ||
}), | ||
40_000 // Increase timeout for slow CI | ||
); | ||
|
||
it('throws after being disposed', () => | ||
sandbox(async dir => { | ||
dir.write({'input.scss': '$a: b; c {d: $a}'}); | ||
await compiler.dispose(); | ||
expect(() => compiler.compileAsync(dir('input.scss'))).toThrowError(); | ||
})); | ||
|
||
it('waits for compilations to finish before disposing', () => | ||
sandbox(async dir => { | ||
let completed = false; | ||
dir.write({'input.scss': '@import "slow"'}); | ||
const compilation = compiler.compileAsync(dir('input.scss'), { | ||
importers: [getSlowImporter(() => (completed = true))], | ||
}); | ||
expect(completed).toBeFalse(); | ||
await compiler.dispose(); | ||
expect(completed).toBeTrue(); | ||
await expectAsync(compilation).toBeResolved(); | ||
})); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright 2023 Google Inc. Use of this source code is governed by an | ||
// MIT-style license that can be found in the LICENSE file or at | ||
// https://opensource.org/licenses/MIT. | ||
|
||
import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; | ||
import {initAsyncCompiler, initCompiler, SassString} from 'sass'; | ||
|
||
import {spy, URL} from './utils'; | ||
|
||
export const functions = { | ||
'foo($args)': (args: unknown) => new SassString(`${args}`), | ||
}; | ||
|
||
export const importers = [ | ||
{ | ||
canonicalize: (url: string) => new URL(`u:${url}`), | ||
load: (url: typeof URL) => ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think now that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
contents: `.import {value: ${url.pathname}} @debug "imported";`, | ||
syntax: 'scss' as const, | ||
}), | ||
}, | ||
]; | ||
|
||
export const asyncImporters = [ | ||
{ | ||
canonicalize: (url: string) => | ||
Promise.resolve(importers[0].canonicalize(url)), | ||
load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), | ||
}, | ||
]; | ||
|
||
export const getLogger = () => ({debug: spy(() => {})}); | ||
|
||
/* A slow importer that executes a callback after a delay */ | ||
export const getSlowImporter = (callback: () => void) => ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declare functions as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved in 6dcdd38 |
||
canonicalize: async () => new URL('foo:bar'), | ||
load: async () => { | ||
await new Promise(resolve => setTimeout(resolve, 100)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than using a timeout when testing that concurrent systems wait for a given event, it's usually better to have a promise whose resolution you control explicitly so you can guarantee it won't finish until you want it to (and then you don't end up writing tests that take a lot longer than they need to because of delays). You can run There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved in 6dcdd38 |
||
callback(); | ||
return {contents: '', syntax: 'scss' as const}; | ||
}, | ||
}); | ||
|
||
describe('Compiler', () => { | ||
let compiler: Compiler; | ||
|
||
beforeEach(() => { | ||
compiler = initCompiler(); | ||
}); | ||
|
||
afterEach(() => { | ||
compiler.dispose(); | ||
}); | ||
|
||
describe('compileString', () => { | ||
it('performs complete compilations', () => { | ||
const logger = getLogger(); | ||
const result = compiler.compileString( | ||
'@import "bar"; .fn {value: foo(baz)}', | ||
{importers, functions, logger} | ||
); | ||
Comment on lines
+75
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not sure I 100% understand the request, but this is my attempt to do it by using importers and functions similar to the async case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to testing running multiple synchronous compilations at the same time. This means you'll need to start a second compilation within a callback for the first one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in 7c17ae5 |
||
expect(result.css).toEqualIgnoringWhitespace( | ||
'.import {value: bar;} .fn {value: "baz";}' | ||
); | ||
expect(logger.debug).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('performs compilations in callbacks', () => { | ||
const nestedImporter = { | ||
canonicalize: () => new URL('foo:bar'), | ||
load: () => ({ | ||
contents: compiler.compileString('x {y: z}').css, | ||
syntax: 'scss' as const, | ||
}), | ||
}; | ||
const result = compiler.compileString('@import "nested"; a {b: c}', { | ||
importers: [nestedImporter], | ||
}); | ||
expect(result.css).toEqualIgnoringWhitespace('x {y: z;} a {b: c;}'); | ||
}); | ||
|
||
it('throws after being disposed', () => { | ||
compiler.dispose(); | ||
expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('AsyncCompiler', () => { | ||
let compiler: AsyncCompiler; | ||
const runs = 1000; // Number of concurrent compilations to run | ||
|
||
beforeEach(async () => { | ||
compiler = await initAsyncCompiler(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await compiler.dispose(); | ||
}); | ||
|
||
describe('compileStringAsync', () => { | ||
it('handles multiple concurrent compilations', async () => { | ||
const logger = getLogger(); | ||
const compilations = Array(runs) | ||
.fill(0) | ||
.map((_, i) => | ||
compiler.compileStringAsync( | ||
`@import "${i}"; .fn {value: foo(${i})}`, | ||
{importers: asyncImporters, functions, logger} | ||
) | ||
); | ||
Array.from(await Promise.all(compilations)) | ||
.map((result: CompileResult) => result.css) | ||
.forEach((result, i) => { | ||
expect(result).toEqualIgnoringWhitespace( | ||
`.import {value: ${i};} .fn {value: "${i}";}` | ||
); | ||
}); | ||
expect(logger.debug).toHaveBeenCalledTimes(runs); | ||
}, 15_000); // Increase timeout for slow CI | ||
|
||
it('throws after being disposed', async () => { | ||
await compiler.dispose(); | ||
expect(() => | ||
compiler.compileStringAsync('$a: b; c {d: $a}') | ||
).toThrowError(); | ||
}); | ||
|
||
it('waits for compilations to finish before disposing', async () => { | ||
let completed = false; | ||
const compilation = compiler.compileStringAsync('@import "slow"', { | ||
importers: [getSlowImporter(() => (completed = true))], | ||
}); | ||
expect(completed).toBeFalse(); | ||
await compiler.dispose(); | ||
expect(completed).toBeTrue(); | ||
await expectAsync(compilation).toBeResolved(); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Rather than
as const
, I think it's cleaner to add a type declaration to the variable itself (const nestedImporter: Importer
) so that it infers the correct types for all its components. Similarly for the top-level fields incompiler.test.ts
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in 0a14b6f.