@syncerauk/test-rules 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,9 @@ Both in the repo's `scripts/test-rules/`:
49
49
 
50
50
  - `config.json`: what differs between repos. `fail` (false is report only), `tests` and
51
51
  `ignore` (which files are test files), `fixtures` (folders a test may read), `wording` (the
52
- one list of what a person reads), `reads` (the reads register: `"comment"`, or
52
+ one list of what a person reads), `style_contract` (`{ "<test file>": ["<.css or .scss path or
53
+ glob>"] }`: the one test that may read those stylesheets; style rules are checked there, once),
54
+ `reads` (the reads register: `"comment"`, or
53
55
  `{ "json": "<path>" }` for the map under `"tests"` in that file, `"key": "<name>"` for another
54
56
  key, `"key": null` when the file itself is the map). The defaults are `CONFIG_DEFAULTS` in the
55
57
  script.
@@ -67,6 +69,14 @@ A repo starts with `"fail": false`: breaches are printed and the exit is 0. One
67
69
 
68
70
  ## Versions
69
71
 
72
+ - **1.0.4** — `style_contract`: a repo may name one test that reads its stylesheets (`.css`,
73
+ `.scss`), the way wording is read once from the wording list. Those files are not a
74
+ `T4:source-text` breach in that test, and only in that test. Nothing changes for a repo that
75
+ does not set it.
76
+ - **1.0.3** — A JSON reads register no longer turns off the scan of the test's code. A source
77
+ file the test builds a path to from its own place, in a test that reads files, is a
78
+ `T4:source-text` breach unless the test declares it `// runs:`. A repo with a JSON register
79
+ (app, portal) can see breaches that are not on its list on its first run of 1.0.3.
70
80
  - **1.0.2** — A path that starts with a dot-folder (`.github/workflows/gate.yml`) is a repo
71
81
  path; only `./` and `../` are relative to the test. A JSON reads register may be the whole
72
82
  file (`"key": null`); a register with no map at its key cannot run (exit 3) instead of reading
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncerauk/test-rules",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "The static check of Syncera's test-writing rules (T1, T4, T6 and the line limit of R7). One file, no dependency. Published to npm by the orchestrator's publish-test-rules workflow; each repo pins an exact version.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/test-rules.mjs CHANGED
@@ -33,11 +33,14 @@
33
33
  // T4:source-text the test reads a source file as text: a read it declares (the repo's reads
34
34
  // register) that is a source file or a folder and is neither under a fixture
35
35
  // folder nor on the wording list; a `?raw` import or a raw import.meta.glob;
36
- // and, where the register is comment lines or absent, a source file whose
37
- // path the test builds from its own place (new URL(…, import.meta.url), or
38
- // join( / resolve( on import.meta.dirname, __dirname, ROOT or HERE) in a test
39
- // that reads the file system and declares it neither `// reads:` nor
40
- // `// runs:` (a file that is only run is declared `// runs:`)
36
+ // and, whatever the register's form, a source file whose path the test
37
+ // builds from its own place (new URL(…, import.meta.url), or join( /
38
+ // resolve( on import.meta.dirname, __dirname, ROOT or HERE) in a test that
39
+ // reads the file system and declares it neither `// reads:`, `// runs:` nor
40
+ // in its JSON register entry (a file that is only run is declared `// runs:`).
41
+ // A JSON register is what a test says it reads; it does not stop the code
42
+ // being read for what it does read (1.0.3). A stylesheet named for a test in
43
+ // `style_contract` is not a breach in that test, and only in that test (1.0.4).
41
44
  // T6:timer setTimeout( / setInterval( / sleep( / timers/promises with no fake timers in the file
42
45
  // T6:clock Date.now( / new Date() / performance.now( with no fake timers in the file
43
46
  // T6:outside homedir(), process.env.HOME, a relative path that climbs out of the repo,
@@ -69,6 +72,7 @@ export const CONFIG_DEFAULTS = Object.freeze({
69
72
  ignore: [], // test files the rules do not judge (a real-time tier 3 suite)
70
73
  fixtures: [], // folders a test may read: `a/fixtures/`
71
74
  wording: [], // the one list of what a person reads (a built page, a legal text): files, globs, folders
75
+ style_contract: {}, // { "<test file>": ["<.css/.scss path or glob>", …] }: the one test that may read those stylesheets
72
76
  reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" } (key null: the file itself is the map)
73
77
  source_ext: ['mjs', 'cjs', 'js', 'jsx', 'ts', 'tsx', 'mts', 'cts', 'sql', 'css', 'scss', 'html', 'sh', 'yml', 'yaml', 'md', 'mdx'],
74
78
  max_lines: 1500,
@@ -271,7 +275,9 @@ function envLine({ code, bare }, allowed) {
271
275
  /** T4: the line of the first read of a source file as text, or 0. */
272
276
  function sourceTextLine({ path, text, code, bare, strings }, { config, register, tracked }) {
273
277
  const isSource = (p) => config.source_ext.includes(String(p).split('.').pop().toLowerCase());
274
- const allowed = (p) => under(config.fixtures, p) || under(config.wording, p);
278
+ // A style contract is the one test that may read the stylesheets it names: style rules are
279
+ // checked once, there, the way wording is checked once from the wording list.
280
+ const allowed = (p) => under(config.fixtures, p) || under(config.wording, p) || under(config.style_contract?.[path], p);
275
281
  const isFolderOrGlob = (p) => (/[*?]/.test(p) ? !/\.[A-Za-z0-9]+$/.test(p) || isSource(p) : String(p).endsWith('/'));
276
282
  const offends = (p) => !allowed(p) && (isFolderOrGlob(p) || isSource(p));
277
283
 
@@ -291,8 +297,8 @@ function sourceTextLine({ path, text, code, bare, strings }, { config, register,
291
297
  if (/\?raw|as\s*:\s*['"]raw['"]/.test(call)) return lineOf(code, m.index);
292
298
  }
293
299
  // A source file whose path the test builds from its own place, in a test that reads the file
294
- // system, where no register says what the test reads: declared `// runs:` when it is only run.
295
- if (register && typeof register === 'object') return 0;
300
+ // system, that the test does not declare: `// runs:` when it is only run. A JSON register does
301
+ // not silence this: a register can miss a read, and a source file it names returned above.
296
302
  if (!FS_READ.test(code) || !tracked) return 0;
297
303
  const said = [...declared.reads, ...declared.runs].map((r) => r.pattern);
298
304
  for (const s of strings) {
@@ -395,6 +401,11 @@ export function withDefaults(raw) {
395
401
  for (const k of ['tests', 'ignore', 'fixtures', 'wording', 'source_ext', 'task_names', 'fake_timers', 'env_allowed']) {
396
402
  if (!Array.isArray(config[k]) || config[k].some((x) => typeof x !== 'string')) throw fail(EXIT.FAILED, `${CONFIG_FILE}: ${k} must be a list of strings`);
397
403
  }
404
+ const sc = config.style_contract;
405
+ const styleOnly = (g) => typeof g === 'string' && /\.s?css$/.test(g);
406
+ if (!sc || typeof sc !== 'object' || Array.isArray(sc) || Object.values(sc).some((v) => !Array.isArray(v) || !v.length || !v.every(styleOnly))) {
407
+ throw fail(EXIT.FAILED, `${CONFIG_FILE}: style_contract must be { "<test file>": ["<path or glob ending .css or .scss>", …] }`);
408
+ }
398
409
  const r = config.reads;
399
410
  const json = r && typeof r === 'object' && !Array.isArray(r) && typeof r.json === 'string'
400
411
  && Object.keys(r).every((k) => k === 'json' || k === 'key') && (r.key === undefined || r.key === null || typeof r.key === 'string');