jest-csl
This is a library to make testing Citation Style Language definitions
easier using Jest
. It includes:
citeproc-js
ls
, cd
, mkdir
and friends.Node.js
installed. Run node -v
to check it's version 10
or later.yarn
with npm install -g yarn
, or mentally replace
yarn
in the rest of this document with npm
.This step is fairly flexible, but the following will work and is a good place to start.
my-style-repo
├── src
│ ├── style.csl
│ └── juris-au-style.csl (jurisdiction override, if you are using CSL-M)
├── test
│ ├── corpus.json
│ ├── style.test.js
│ └── style.spec.yaml
├── package.json
├── .gitignore
└── jest.config.js
Name your items according to whatever your style is called, like
chicago-author-date.csl
instead of style.csl
.
package.json
Add a package.json with the following contents:
{
"scripts": {
"test": "jest --watchAll --noStackTrace"
}
}
Then, run yarn add -D jest jest-csl
. You will get the latest version of
jest
and this library (jest-csl
).
test/corpus.json
This file should not be built by hand, it is too tedious. Instead:
Right-Click > Export Library ...
; select Better CSL JSON
and tick the Keep updated
box./path/to/your-style-repo/test/corpus.json
.test/style.spec.yaml
This is where you write your tests. See below.
test/style.test.js
: a test configurationInsert the following contents, modifying STYLE
and optionally un-commenting
or deleting jurisdictionDirs
as required for your own style:
module.exports = {
csl: "./src/STYLE.csl",
// use jurisdictionDirs if you are using CSL-M and want to include overrides
// jurisdictionDirs: ["./src"],
libraries: ["./test/corpus.json"],
suites: ["./test/STYLE.spec.yaml"]
};
const { jestCSL } = require('jest-csl');
jestCSL(module.exports);
jest.config.js
This file lets jest-csl
check that the locales are all cached before running.
module.exports = {
globalSetup: "jest-csl/setup"
};
It won't update the cache. That would be too slow to do on every test run. See 'Updating the cache' below.
.gitignore
You're probably going to want to put all of this in Git at some point, so add these contents:
node_modules
Note: This test suite was developed for a footnote-based style. If you're building a different kind of style (i.e. trying to test bibliography output), we might need to add a new kind of test, so please file an issue.
Specs are written in YAML. If you haven't written YAML before, read this primer.
The YAML spec files are structured somewhat like a Jest test suite. Here's an example:
- describe: "Name of a feature of this CSL to test (often a kind of document)"
tests:
- it: "should render a basic book citation"
single: { id: "doe2001" }
# you can use > to join multiple lines with spaces if a line gets long
expect: "Doe, <i>Miscellaneous Writings</i>, 2001."
- it: "should include both volume and issue in a journal article"
single: { id: "doe2003" }
# you can use > to join multiple lines with spaces if a line gets long
# <i></i> is italics, for other formats just run the test first
expect: >
John Doe, 'A Journal Article Written in My Diary' (2003) 89(3)
<i>John's Diary</i> 124.
For more complex combined citations, use 'sequence' to test the in-texts/footnotes generated for a sequence of clusters of cites.
- describe: "..."
tests:
...
- describe: "Subsequent references"
tests:
- it: "should render plain ibids for the same locator"
sequence:
- [ { id: "doe2001", locator: "5", label: "page" }
, { id: "doe2001", locator: "5", label: "page", prefix: "see also ", suffix: " etc" } ]
- [{ id: "doe2001", locator: "5", label: "page" }]
expect:
- Doe, <i>Miscellaneous Writings</i>, 2001, p. 5; see also <i>ibid</i> etc.
- <i>Ibid</i>.
To test the abbreviations found in the Abbreviation Filter:
- describe: "a"
tests:
- it: "should x"
...
abbreviations:
- hereinafter:
doe2001: Misc
- jurisdiction: us
container-title:
Coolest Tribunal Of The Land: CTL
Without a jurisdiction
specified, it is default
.
See this command's output for a list of allowed categories.
node -e 'var c=require('citeproc');console.log(Object.keys(new c.AbbreviationSegments()))'
You can add macro: MacroName
to a test, and for that test only, your style's
<layout>
tag will have its contents replaced with <text macro="MacroName" />
, and any prefix and suffix is stripped (delimiter is kept). This is convenient
when you are building a style from the ground up and can't write full-item tests yet.
<macro name="MacroName">
<text variable="locator" />
</macro>
<citation ...>
<layout prefix="(" suffix=")" delimiter="; " ...>
<text value="lots of messy code" />
<choose>
...
</choose>
</layout>
</citation>
- describe: "MacroName only"
tests:
- it: "should render a locator"
macro: MacroName
single: { id: "citekey", locator: "a-locator" }
expect: "a-locator"
Sometimes you will have sets of tests that you want to pass for two different
styles, and then extended suites that only apply to one style. This might
encompass two variations on one style, or just the plain CSL vs CSL-M versions
of your style. It would make sense to have more than one test configuration
(.test.js
file) to exercise more than one combination.
You can pass multiple suites in your test configuration file. They will be
merged according to the order; later styles with the same describe > it
combinations will override previous ones.
# core.spec.yaml
- describe: "Unit"
tests:
- it: "should a"
...
- it: "should b"
...
# extended.spec.yaml
- describe: "Unit"
tests:
- it: "should b"
single: "override"
expect: "override"
- it: "should c"
single: "new"
expect: "new"
Setting
{
// ...
suites: ["./test/core.spec.yaml", "./test/extended.spec.yaml"]
}
Results in all three tests being run, with "should a" preserved, "should b" overridden and "should c" added.
For convenience, you can split a large test suite into multiple files, and combine them all with a glob. Use a level of directories or a naming scheme to separate groups that should be strictly before or after one another.
{
// ...
suites: ["./test/core/*.yaml", "./test/extended/*.yaml"]
}
The same works for jurisdictionDirs
and libraries
.
You can add a mode
to a test with the value skip
or only
or known
. If
it's skip
, it will be skipped; if >= 1 test has only
, every test that isn't
labelled only
will be skipped. known
means it will be skipped in Jest, but
will be included in the results output (for documenting failures but not
polluting your test runner).
Also, any test that does not have an expect
key will
be skipped as a test stub, so you can write lots of unwritten it
keys as a to-do
list.
- describe: "Unit"
tests:
- it: "should b"
mode: skip
...
yarn test
Follow the onscreen instructions to interact with the Jest environment (or quit).
yarn jest-csl update
This will attempt to update your cached locales and style-modules.
Before, you defined a test configuration that would export a plain-object description of itself if it wasn't running in Jest. You can use that now.
output
key on the configuration object, with a string path to a
JSON file to be written.jest-csl results
on your configuration.// ./test/my-configuration.test.js
module.exports = {
...,
output: './results.json'
}
yarn jest-csl results --includeLibrary ./test/my-configuration.test.js
jq . results.json
{
"library": {
"citekey": { ... },
...
},
"units": [
{
"describe": "...",
"tests": [...]
}
]
}
Each test object has these keys added:
type
indicating what kind of test it was:
single
sequence
doc
for documentation only (i.e. it had a doc key but no actual test)stub
otherwiseresult
key of the same shape as expect
(only if the test had an expect
)Two keys, doc
and meta
, are recognised as metadata on both describe
units
and test cases. They may have any content, and will be included in the output.
const { cslTestResults } = require('jest-csl');
const config = require('./test/my-configuration.test');
let { engine, units, library, citeIds } = cslTestResults(config);
console.log(units);
This generates a JS array of each of the test units, with each test transformed as described in the CLI. This is useful for generating documentation or making a custom view of the results, a bit like a jest reporter but without losing the test case information and metadata.
engine
is a wrapper around CSL.Engine pre-loaded with your libraries.
engine.retrieveItem('citeKey')
may be useful to you.