Skip to content

Commit cd80158

Browse files
committed
Merge branch 'test-support'
2 parents 1f10226 + 65fc68e commit cd80158

File tree

11 files changed

+357
-75
lines changed

11 files changed

+357
-75
lines changed

packages/@css-blocks/core/src/BlockParser/features/export-blocks.ts

+37-34
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,29 @@ export async function exportBlocks(block: Block, factory: BlockFactory | BlockFa
5959
`Malformed block export: \`@export ${atRule.params}\``,
6060
sourceRange(factory.configuration, block.stylesheet, file, atRule),
6161
));
62-
}
62+
} else {
63+
block.blockAST!.children.push(exports);
64+
// Import file, then parse file, then save block reference.
65+
let srcBlockPromise: Promise<Block> = Promise.resolve(block);
66+
if (typeguards.isBlockExport(exports)) {
67+
srcBlockPromise = Promise.resolve(factory.getBlockRelative(block.identifier, exports.fromPath));
68+
}
6369

64-
// Import file, then parse file, then save block reference.
65-
let srcBlockPromise: Promise<Block> = Promise.resolve(block);
66-
if (typeguards.isBlockExport(exports)) {
67-
srcBlockPromise = Promise.resolve(factory.getBlockRelative(block.identifier, exports.fromPath));
70+
// Validate our imported block name is a valid CSS identifier.
71+
const exportPromise = srcBlockPromise.then(
72+
(srcBlock) => {
73+
validateAndAddBlockExport(block, factory.configuration, srcBlock, remoteNames, exports, file, atRule);
74+
},
75+
(error) => {
76+
block.addError(new errors.CascadingError(
77+
`Error in exported block "${(<BlockExport>exports).fromPath}"`,
78+
error,
79+
sourceRange(factory.configuration, block.stylesheet, file, atRule),
80+
));
81+
},
82+
);
83+
exportPromises.push(exportPromise);
6884
}
69-
70-
// Validate our imported block name is a valid CSS identifier.
71-
const exportPromise = srcBlockPromise.then(
72-
(srcBlock) => {
73-
validateAndAddBlockExport(block, factory.configuration, srcBlock, remoteNames, exports, file, atRule);
74-
},
75-
(error) => {
76-
block.addError(new errors.CascadingError(
77-
`Error in exported block "${(<BlockExport>exports).fromPath}"`,
78-
error,
79-
sourceRange(factory.configuration, block.stylesheet, file, atRule),
80-
));
81-
},
82-
);
83-
exportPromises.push(exportPromise);
8485
});
8586
}
8687

@@ -113,20 +114,22 @@ export function exportBlocksSync(block: Block, factory: BlockFactorySync, file:
113114
`Malformed block export: \`@export ${atRule.params}\``,
114115
sourceRange(factory.configuration, block.stylesheet, file, atRule),
115116
));
116-
}
117-
118-
// Import file, then parse file, then save block reference.
119-
let srcBlock: Block;
120-
if (typeguards.isBlockExport(exports)) {
121-
try {
122-
srcBlock = factory.getBlockRelative(block.identifier, exports.fromPath);
123-
validateAndAddBlockExport(block, factory.configuration, srcBlock, remoteNames, exports, file, atRule);
124-
} catch (error) {
125-
block.addError(new errors.CascadingError(
126-
`Error in exported block "${exports.fromPath}"`,
127-
error,
128-
sourceRange(factory.configuration, block.stylesheet, file, atRule),
129-
));
117+
} else {
118+
block.blockAST!.children.push(exports);
119+
120+
// Import file, then parse file, then save block reference.
121+
let srcBlock: Block;
122+
if (typeguards.isBlockExport(exports)) {
123+
try {
124+
srcBlock = factory.getBlockRelative(block.identifier, exports.fromPath);
125+
validateAndAddBlockExport(block, factory.configuration, srcBlock, remoteNames, exports, file, atRule);
126+
} catch (error) {
127+
block.addError(new errors.CascadingError(
128+
`Error in exported block "${exports.fromPath}"`,
129+
error,
130+
sourceRange(factory.configuration, block.stylesheet, file, atRule),
131+
));
132+
}
130133
}
131134
}
132135
});

packages/@css-blocks/core/test/acceptance/DfnFilesFullWorkflow.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ export class AcceptanceTestDefinitionFilesFullWorkflow {
1616
const compiler = new BlockCompiler(postcss, config);
1717
compiler.setDefinitionCompiler(blockDfnCompiler);
1818

19+
imports.registerSource(
20+
"/foo/bar/other.block.css",
21+
`:scope { color: red; }`,
22+
);
23+
1924
// Part 1: Given a basic CSS Blocks source file, generate a definition file.
2025
imports.registerSource(
2126
"/foo/bar/source.block.css",
2227
outdent`
28+
@block (default as other) from './other.block.css';
29+
@export (other);
30+
2331
:scope { background-color: #FFF; }
2432
:scope[toggled] { color: #000; }
2533
:scope[foo="bar"] { border-top-color: #F00; }
@@ -32,7 +40,6 @@ export class AcceptanceTestDefinitionFilesFullWorkflow {
3240
`,
3341
);
3442
const originalBlock = await factory.getBlockFromPath("/foo/bar/source.block.css");
35-
3643
const { css: cssTree, definition: definitionTree } = compiler.compileWithDefinition(originalBlock, originalBlock.stylesheet!, new Set(), "/foo/bar/source.block.d.css");
3744
const compiledCss = cssTree.toString();
3845
const definition = definitionTree.toString();
@@ -64,6 +71,8 @@ export class AcceptanceTestDefinitionFilesFullWorkflow {
6471
`,
6572
outdent`
6673
@block-syntax-version 1;
74+
@block other from "./other.css";
75+
@export (other);
6776
:scope {
6877
block-id: "${originalBlock.guid}";
6978
block-name: "source";
@@ -93,12 +102,19 @@ export class AcceptanceTestDefinitionFilesFullWorkflow {
93102
imports.reset();
94103
factory.reset();
95104

105+
imports.registerSource(
106+
"/foo/bar/other.css",
107+
`:scope { color: red; }`,
108+
);
109+
96110
imports.registerCompiledCssSource(
97111
"/foo/bar/source.css",
98112
compiledCss,
99113
"/foo/bar/source.block.d.css",
100114
definition,
101115
);
116+
117+
await factory.getBlockFromPath("/foo/bar/other.css");
102118
const reconstitutedBlock = await factory.getBlockFromPath("/foo/bar/source.css");
103119

104120
// And now some checks to validate that we were able to reconstitute accurately.

packages/@css-blocks/ember-app/README.md

+35
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,41 @@ The following options can be passed as options to the `css-blocks` property in y
3232
* `broccoliConcat` - Options that control the behavior of broccoli-concat, which is used to concatenate CSS files together by ember-app during postprocess. If this is set to false, broccoli-concat will *not* run and you'll need to add additional processing to add the CSS Blocks compiled content to your final CSS build artifact.
3333
* `appClasses` - List of classes that are used by application CSS and might conflict with the optimizer. You should add any short class names (~5 characters) to this list so the optimizer doesn't use these when building the CSS Blocks compiled output. This is a convenience alias for `optimization.rewriteIdents.omitIdents.class[]`. It has no effect if optimization is disabled.
3434

35+
## Testing
36+
Once you have integrated this addon within an ember application, all your CSS classnames are going to look very different from what they did in the block files that you had originally written and the output CSS classnames will vary across builds and across different machines. This is done intentionally to ensure that you don't use any CSS classname selectors in any of the tests and that your tests remain robust across executions. Not to mention, even the number of classes on an element can be entirely different after all the CSS has been optimized using opticss.
37+
38+
In order to faciliate testing in such an environment, this ember addon provides a utility method called `setupCSSBlocksTest()` that that exposes the CSS blocks service that can in turn be used to query the existence of certain classes on your elements.
39+
40+
Note: `setupCSSBlocksTest()` has been written to work with `ember-qunit` and `ember-mocha`.
41+
42+
### Test setup and usage
43+
- In your integration or acceptance tests, call `setupCSSBlocksTest()` declaring any tests. Ensure that `setupCSSBlocksTest()` is called *after* `setupTest|setupRenderingTest|setupApplicationTest` for the setup to work as desired.
44+
**Note: `setupCSSBlocksTest` is exposed within a service on the application's namespace and will have to be imported as such**
45+
```js
46+
import { setupCSSBlocksTest} from '<appName>/services/css-blocks-test-support';
47+
```
48+
- After this, the css-blocks service is available to the test via `this.cssblocks`
49+
- The test service primarily exposes a single API function, `this.cssBlocks.getBlock(<pathToBlock>, <blockName>)` that takes in a path to the block file and the an optional blockName. If the blockName isn't specified, the default block for the block file is returned. The <blockPath> begin with either the appName, the addon name (if its from an in-repo addon) or by the engine's name(in the case of an in-repo engine) that it is a part of. `getBlock()` returns a reference to the runtime CSS block which can be queried for styles within the block using `.style(<styleName>)`.
50+
- The element can then assert whether a certain style is present on it or not using `element.classList.contains()`
51+
52+
Putting it all together in an example,
53+
```js
54+
import { setupCSSBlocksTest } from 'my-very-fine-app/services/css-blocks-test-support';
55+
56+
module('Acceptance | css blocks test', function (hooks) {
57+
setupApplicationTest(hooks);
58+
59+
setupCSSBlocksTest(hooks);
60+
61+
test('visiting /', async function (assert) {
62+
await visit('/');
63+
let defaultBlock = this.cssBlocks.getBlock("my-very-fine-app/styles/components/application", "default");
64+
let element = find('[data-test-large-hello]');
65+
assert.ok(element.classList.contains(defaultBlock.style(':scope[size="large]')));
66+
});
67+
});
68+
```
69+
3570
## Common Gotchas
3671

3772
This section is devoted to common issues you might run into when working with this addon.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../src/TestSupportData.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/// @ts-ignore
2+
import { data as _data } from "./-css-blocks-data";
3+
/// @ts-ignore
4+
import { testSupportData as _testData } from "./-css-blocks-test-support-data";
5+
import { AggregateRewriteData } from "./AggregateRewriteData";
6+
import CSSBlocksService from "./css-blocks";
7+
import { TestSupportData } from "./TestSupportData";
8+
9+
const testData: TestSupportData = _testData;
10+
const data: AggregateRewriteData = _data;
11+
12+
export class TestBlock {
13+
blockName: string;
14+
blockId: string;
15+
styles: Array<string>;
16+
constructor(blockName: string , blockId: string, styles: Array<string>) {
17+
this.blockName = blockName;
18+
this.blockId = blockId;
19+
this.styles = styles;
20+
}
21+
style(styleName: string) {
22+
if (this.styles.includes(styleName)) {
23+
return `${this.blockId}${styleName}`;
24+
} else {
25+
throw new Error(`No style named "${styleName}" found on "${this.blockName}". Specify one of: ${this.styles.join(", ")}.`);
26+
}
27+
}
28+
}
29+
30+
export class CSSBlocksTestService extends CSSBlocksService {
31+
constructor() {
32+
/// @ts-ignore
33+
super(...arguments); // need to pass in ...arguments since "@ember/service" extends from EmberObject
34+
}
35+
36+
classNamesFor(argv: Array<string | number | boolean | null>): string {
37+
let runtimeClassNames = super.classNamesFor(argv);
38+
let directlyAppliedStyleIds = this.getDirectlyAppliedStyles(argv);
39+
// convert the directly applied styleIds into a human readable form
40+
let proxyClassNames = this.getStyleNames(directlyAppliedStyleIds).join(" ");
41+
return `${proxyClassNames} ${runtimeClassNames}`;
42+
}
43+
44+
/**
45+
* Used to query the runtime test data to obtain the block's runtime id from
46+
* it's moduleName
47+
* @param moduleName name of the node module to look for the block
48+
* @param blockName name of the block to find within the moduleName
49+
*/
50+
getBlock(fileName: string, blockName: string): TestBlock {
51+
let runtimeBlockName = testData[fileName];
52+
53+
if (!runtimeBlockName) {
54+
throw new Error(`No block file named "${fileName}" found in within this app's namespace`);
55+
} else {
56+
let runtimeGuid = runtimeBlockName[blockName];
57+
58+
if (!runtimeGuid) {
59+
throw new Error(`No block named "${blockName}" found within "${fileName}". Specify one of: ${Object.keys(runtimeBlockName).join(", ")}.`);
60+
}
61+
return new TestBlock(blockName, runtimeGuid, getStyles(runtimeGuid));
62+
}
63+
}
64+
}
65+
66+
/**
67+
* Returns the available styles, given the blockGuid
68+
*/
69+
function getStyles(guid: string): Array<string> {
70+
let blockIndex = data.blockIds[guid];
71+
let blockInfo = data.blocks[blockIndex];
72+
return Object.keys(blockInfo.blockInterfaceStyles);
73+
}
74+
75+
/**
76+
* This is a utility function that sets up all the testing infra needed for CSS
77+
* blocks. It essentially overrides the css-blocks service used by the app
78+
* during tests and adds dummy classNames that is more human readable. It exposes
79+
* the test API via this.cssBlocks that can be used within the test files themselves.
80+
*
81+
* This function will work both with `ember-qunit` and with `ember-mocha` since
82+
* the ember setup functions do the same for both the libraries.
83+
*
84+
* Usage within a test
85+
module('Acceptance | css blocks helper', function (hooks) {
86+
setupApplicationTest(hooks);
87+
setupCSSBlocksTest(hooks);
88+
89+
test('visiting /', async function (assert) {
90+
await visit('/');
91+
let defaultBlock = this.cssBlocks.getBlock("hue-web-component/styles/components/hue-web-component", "default");
92+
let element = find('[data-test-large-hello]');
93+
assert.ok(element.classList.contains(defaultBlock.style(':scope[size="large]')));
94+
});
95+
});
96+
*/
97+
// @ts-ignore the error for hooks as this can either come from qunit or mocha
98+
export function setupCSSBlocksTest(hooks) {
99+
hooks.beforeEach(function() {
100+
// ember-qunit and ember-mocha sets this.owner and we have to check that one of the
101+
// setup functions from there are called before we can register our service
102+
// @ts-ignore
103+
if (!this.owner) {
104+
throw new Error(
105+
"setupCSSBlocksTest must be called after setupTest|setupRenderingTest|setupApplicationTest",
106+
);
107+
}
108+
109+
// register the CSS blocks service before each test
110+
// @ts-ignore
111+
this.owner.register("service:css-blocks", CSSBlocksTestService);
112+
// @ts-ignore
113+
this.cssBlocks = this.owner.lookup("service:css-blocks");
114+
});
115+
}

0 commit comments

Comments
 (0)