@thegetty/quire-cli 1.0.0-rc.34 → 1.0.0-rc.36
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/CHANGELOG.md +36 -0
- package/README.md +9 -0
- package/bin/cli.js +19 -1
- package/package.json +21 -8
- package/schemas/config.schema.json +194 -0
- package/schemas/figures.schema.json +56 -0
- package/schemas/layout.schema.json +5 -0
- package/schemas/objects.schema.json +62 -0
- package/schemas/publication.schema.json +140 -0
- package/schemas/references.schema.json +29 -0
- package/src/Command.js +26 -6
- package/src/Command.spec.js +99 -0
- package/src/commands/README.md +213 -122
- package/src/commands/build.js +46 -37
- package/src/commands/build.spec.js +113 -0
- package/src/commands/build.test.js +402 -0
- package/src/commands/clean.js +25 -19
- package/src/commands/clean.spec.js +108 -0
- package/src/commands/clean.test.js +260 -0
- package/src/commands/config.js +251 -0
- package/src/commands/config.spec.js +107 -0
- package/src/commands/config.test.js +715 -0
- package/src/commands/create.js +42 -14
- package/src/commands/create.spec.js +112 -0
- package/src/commands/create.test.js +415 -0
- package/src/commands/epub.js +59 -30
- package/src/commands/epub.spec.js +114 -0
- package/src/commands/epub.test.js +503 -0
- package/src/commands/index.js +9 -2
- package/src/commands/info.js +18 -24
- package/src/commands/info.spec.js +64 -0
- package/src/commands/info.test.js +415 -0
- package/src/commands/pdf.js +58 -84
- package/src/commands/pdf.spec.js +114 -0
- package/src/commands/pdf.test.js +464 -0
- package/src/commands/preview.js +26 -31
- package/src/commands/preview.spec.js +97 -0
- package/src/commands/preview.test.js +250 -0
- package/src/commands/use.js +56 -0
- package/src/commands/use.spec.js +61 -0
- package/src/commands/use.test.js +280 -0
- package/src/commands/validate.js +31 -19
- package/src/commands/validate.spec.js +82 -0
- package/src/commands/validate.test.js +234 -0
- package/src/commands/workflows.js +70 -0
- package/src/errors/build/build-failed-error.js +19 -0
- package/src/errors/build/config-field-missing-error.js +20 -0
- package/src/errors/build/config-file-not-found-error.js +20 -0
- package/src/errors/build/index.js +11 -0
- package/src/errors/index.js +48 -0
- package/src/errors/install/dependency-install-error.js +19 -0
- package/src/errors/install/directory-not-empty-error.js +25 -0
- package/src/errors/install/index.js +12 -0
- package/src/errors/install/invalid-path-error.js +31 -0
- package/src/errors/install/invalid-starter-error.js +27 -0
- package/src/errors/install/version-not-found-error.js +35 -0
- package/src/errors/output/epub-generation-error.js +19 -0
- package/src/errors/output/index.js +14 -0
- package/src/errors/output/invalid-epub-library-error.js +20 -0
- package/src/errors/output/invalid-pdf-library-error.js +20 -0
- package/src/errors/output/missing-build-output-error.js +21 -0
- package/src/errors/output/pdf-generation-error.js +24 -0
- package/src/errors/output/tool-not-found-error.js +37 -0
- package/src/errors/project/index.js +10 -0
- package/src/errors/project/not-in-project-error.js +20 -0
- package/src/errors/project/project-create-error.js +21 -0
- package/src/errors/quire-error.js +27 -0
- package/src/errors/validation/validation-error.js +20 -11
- package/src/helpers/clean.js +1 -1
- package/src/helpers/docs-url.js +32 -0
- package/src/helpers/test-cwd.js +5 -6
- package/src/helpers/test-cwd.test.js +192 -0
- package/src/helpers/which.js +10 -4
- package/src/lib/11ty/README.md +135 -19
- package/src/lib/11ty/api.js +176 -93
- package/src/lib/11ty/cli.js +77 -35
- package/src/lib/11ty/index.js +64 -5
- package/src/lib/11ty/index.test.js +655 -0
- package/src/lib/README.md +275 -0
- package/src/lib/commander/index.js +100 -0
- package/src/lib/commander/index.test.js +86 -0
- package/src/lib/commander/options.js +195 -0
- package/src/lib/commander/options.test.js +109 -0
- package/src/lib/conf/README.md +84 -73
- package/src/lib/conf/config.js +5 -3
- package/src/lib/conf/config.test.js +281 -0
- package/src/lib/conf/defaults.js +44 -0
- package/src/lib/conf/format.js +60 -0
- package/src/lib/conf/format.test.js +106 -0
- package/src/lib/conf/helpers.js +91 -0
- package/src/lib/conf/helpers.test.js +136 -0
- package/src/lib/conf/index.js +22 -0
- package/src/lib/conf/schema.js +53 -8
- package/src/lib/epub/README.md +133 -2
- package/src/lib/epub/engines.js +46 -0
- package/src/lib/epub/epub.js +36 -11
- package/src/lib/epub/index.js +111 -21
- package/src/lib/epub/index.test.js +518 -0
- package/src/lib/epub/pandoc.js +33 -4
- package/src/lib/epub/pandoc.test.js +122 -0
- package/src/lib/epub/schema.js +21 -0
- package/src/lib/error/README.md +170 -0
- package/src/lib/error/handler.js +107 -0
- package/src/lib/git/README.md +151 -2
- package/src/lib/git/index.js +217 -13
- package/src/lib/git/index.spec.js +80 -0
- package/src/lib/git/index.test.js +453 -0
- package/src/lib/installer/index.js +309 -0
- package/src/lib/installer/index.spec.js +83 -0
- package/src/lib/installer/index.test.js +545 -0
- package/src/lib/logger/README.md +424 -0
- package/src/lib/logger/debug.js +90 -0
- package/src/lib/logger/debug.spec.js +130 -0
- package/src/lib/logger/index.js +228 -0
- package/src/lib/logger/index.spec.js +131 -0
- package/src/lib/logger/index.test.js +477 -0
- package/src/lib/npm/README.md +127 -0
- package/src/lib/npm/index.js +198 -0
- package/src/lib/npm/index.spec.js +60 -0
- package/src/lib/npm/index.test.js +355 -0
- package/src/lib/pdf/README.md +131 -0
- package/src/lib/pdf/engines.js +46 -0
- package/src/lib/pdf/index.js +124 -21
- package/src/lib/pdf/index.test.js +708 -0
- package/src/lib/pdf/paged.js +100 -52
- package/src/lib/pdf/paged.test.js +366 -0
- package/src/lib/pdf/prince.js +115 -37
- package/src/lib/pdf/prince.test.js +202 -0
- package/src/lib/pdf/schema.js +21 -0
- package/src/lib/pdf/split.js +61 -33
- package/src/lib/pdf/split.test.js +445 -0
- package/src/lib/process/manager.js +110 -0
- package/src/lib/process/manager.test.js +55 -0
- package/src/lib/project/build.js +143 -0
- package/src/lib/project/build.test.js +253 -0
- package/src/lib/project/config.js +48 -0
- package/src/lib/project/config.test.js +134 -0
- package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
- package/src/lib/project/detect.test.js +157 -0
- package/src/lib/project/index.js +40 -0
- package/src/lib/project/paths.js +224 -0
- package/src/lib/project/version.js +110 -0
- package/src/lib/project/version.test.js +350 -0
- package/src/lib/reporter/README.md +211 -2
- package/src/lib/reporter/index.js +512 -0
- package/src/lib/reporter/index.test.js +593 -0
- package/src/main.js +134 -47
- package/src/main.spec.js +61 -0
- package/src/main.test.js +347 -0
- package/src/validators/utils.js +2 -1
- package/src/commands/conf.js +0 -43
- package/src/commands/version.js +0 -43
- package/src/lib/11ty/paths.js +0 -103
- package/src/lib/i18n/README.md +0 -3
- package/src/lib/i18n/config.js +0 -53
- package/src/lib/i18n/index.js +0 -43
- package/src/lib/i18n/localeService.js +0 -58
- package/src/lib/quire/README.md +0 -19
- package/src/lib/quire/index.js +0 -350
package/src/Command.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import config from '#src/lib/conf/config.js'
|
|
2
|
+
import createLogger from '#lib/logger/index.js'
|
|
3
|
+
import createDebug from '#debug'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Command
|
|
@@ -13,10 +15,13 @@ export default class Command {
|
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* @typedef CommandDefinition
|
|
16
|
-
* @property {String} name
|
|
17
|
-
* @property {String} alias
|
|
18
|
-
* @property {Array<String>} aliases
|
|
19
|
-
* @property {String}
|
|
18
|
+
* @property {String} name - Command name used to invoke it
|
|
19
|
+
* @property {String} alias - Single alias for the command
|
|
20
|
+
* @property {Array<String>} aliases - Multiple aliases for the command
|
|
21
|
+
* @property {String} description - Full description shown in command's own help
|
|
22
|
+
* @property {String} summary - One-line summary shown in parent help listing
|
|
23
|
+
* @property {String} [docsLink] - Path appended to docs base URL (e.g., 'quire-commands/#output-files')
|
|
24
|
+
* @property {String} [helpText] - Custom help text shown after built-in help (examples, notes)
|
|
20
25
|
* @property {Array<CommandArgument>} args
|
|
21
26
|
* @property {Array<CommandOption>} options
|
|
22
27
|
* @property {String} version
|
|
@@ -32,12 +37,24 @@ export default class Command {
|
|
|
32
37
|
* @param {CommandDefinition} definition The definition
|
|
33
38
|
*/
|
|
34
39
|
constructor(definition) {
|
|
35
|
-
if (this.constructor
|
|
40
|
+
if (this.constructor === Command) {
|
|
36
41
|
throw new Error('Command is an *abstract* class')
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
this.config = config // quire-cli configuration
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Command-specific debug instance for internal debugging
|
|
48
|
+
* Enable via: DEBUG=quire:commands:name or DEBUG=quire:commands:*
|
|
49
|
+
*/
|
|
50
|
+
this.debug = createDebug(`commands:${definition.name}`)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Command-specific logger with prefix including command name
|
|
54
|
+
* Output format: [quire] LEVEL commands:name message
|
|
55
|
+
*/
|
|
56
|
+
this.logger = createLogger(`commands:${definition.name}`)
|
|
57
|
+
|
|
41
58
|
/**
|
|
42
59
|
* Merge and deduplicate command definition alias and aliases
|
|
43
60
|
* Nota bene: Only the first command alias is displayed in the help.
|
|
@@ -48,6 +65,9 @@ export default class Command {
|
|
|
48
65
|
this.name = definition.name
|
|
49
66
|
this.aliases = definition.aliases
|
|
50
67
|
this.description = definition.description
|
|
68
|
+
this.summary = definition.summary
|
|
69
|
+
this.docsLink = definition.docsLink
|
|
70
|
+
this.helpText = definition.helpText
|
|
51
71
|
this.args = definition.args
|
|
52
72
|
this.options = definition.options
|
|
53
73
|
this.version = definition.version
|
|
@@ -55,7 +75,7 @@ export default class Command {
|
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
definition() {
|
|
58
|
-
return this.
|
|
78
|
+
return this.constructor.definition
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
action() {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import Command from './Command.js'
|
|
2
|
+
import test from 'ava'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Command Base Class Unit Tests
|
|
6
|
+
*
|
|
7
|
+
* Tests the abstract Command base class API contract.
|
|
8
|
+
* These tests verify the class cannot be instantiated directly
|
|
9
|
+
* and that the base methods behave correctly.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test('Command cannot be instantiated directly', (t) => {
|
|
13
|
+
const error = t.throws(() => {
|
|
14
|
+
new Command({ name: 'test', description: 'test command' })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
t.truthy(error)
|
|
18
|
+
t.regex(error.message, /abstract/i, 'error message should mention abstract')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('Command.action() throws not-implemented error', (t) => {
|
|
22
|
+
// Create a minimal subclass that does not override action
|
|
23
|
+
class TestCommand extends Command {
|
|
24
|
+
constructor() {
|
|
25
|
+
super({ name: 'test', description: 'test command' })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const command = new TestCommand()
|
|
30
|
+
const error = t.throws(() => command.action())
|
|
31
|
+
|
|
32
|
+
t.truthy(error)
|
|
33
|
+
t.regex(error.message, /not been implemented/i, 'error message should indicate not implemented')
|
|
34
|
+
t.regex(error.message, /test/i, 'error message should include the command name')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('subclass can override action without error', (t) => {
|
|
38
|
+
class TestCommand extends Command {
|
|
39
|
+
constructor() {
|
|
40
|
+
super({ name: 'test', description: 'test command' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
action() {
|
|
44
|
+
return 'action executed'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const command = new TestCommand()
|
|
49
|
+
const result = command.action()
|
|
50
|
+
|
|
51
|
+
t.is(result, 'action executed')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('Command constructor sets properties from definition', (t) => {
|
|
55
|
+
const definition = {
|
|
56
|
+
name: 'test',
|
|
57
|
+
description: 'A test command',
|
|
58
|
+
summary: 'test summary',
|
|
59
|
+
aliases: ['t', 'tst'],
|
|
60
|
+
args: [['<input>', 'input file']],
|
|
61
|
+
options: [['--verbose', 'enable verbose output']],
|
|
62
|
+
version: '1.0.0',
|
|
63
|
+
hidden: true,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class TestCommand extends Command {
|
|
67
|
+
constructor() {
|
|
68
|
+
super(definition)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
action() {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const command = new TestCommand()
|
|
75
|
+
|
|
76
|
+
t.is(command.name, 'test')
|
|
77
|
+
t.is(command.description, 'A test command')
|
|
78
|
+
t.is(command.summary, 'test summary')
|
|
79
|
+
t.deepEqual(command.aliases, ['t', 'tst'])
|
|
80
|
+
t.deepEqual(command.args, [['<input>', 'input file']])
|
|
81
|
+
t.deepEqual(command.options, [['--verbose', 'enable verbose output']])
|
|
82
|
+
t.is(command.version, '1.0.0')
|
|
83
|
+
t.is(command.hidden, true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('Command constructor injects shared configuration', (t) => {
|
|
87
|
+
class TestCommand extends Command {
|
|
88
|
+
constructor() {
|
|
89
|
+
super({ name: 'test', description: 'test command' })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
action() {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const command = new TestCommand()
|
|
96
|
+
|
|
97
|
+
t.truthy(command.config, 'config should be injected')
|
|
98
|
+
t.is(typeof command.config.get, 'function', 'config should have get method')
|
|
99
|
+
})
|
package/src/commands/README.md
CHANGED
|
@@ -1,201 +1,292 @@
|
|
|
1
|
-
|
|
1
|
+
# Quire CLI Commands
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This directory contains the Quire CLI command implementations and their corresponding tests.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Command Class Pattern
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
quire build
|
|
9
|
-
```
|
|
7
|
+
All Quire CLI commands extend the base `Command` class and follow a consistent pattern:
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
### Command Structure
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
```javascript
|
|
12
|
+
import Command from '#src/Command.js'
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export default class MyCommand extends Command {
|
|
15
|
+
static definition = {
|
|
16
|
+
name: 'my-command',
|
|
17
|
+
description: 'Description of what the command does',
|
|
18
|
+
summary: 'short summary for help text',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
args: [
|
|
21
|
+
['<required-arg>', 'description of required argument'],
|
|
22
|
+
['[optional-arg]', 'description of optional argument', 'default-value']
|
|
23
|
+
],
|
|
24
|
+
options: [
|
|
25
|
+
['-f', '--flag', 'description of flag'],
|
|
26
|
+
['-o', '--option <value>', 'description of option with value', 'default']
|
|
27
|
+
]
|
|
28
|
+
}
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
constructor() {
|
|
31
|
+
super(MyCommand.definition)
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
async action(args, options, command) {
|
|
35
|
+
// Use this.debug for developer debugging (controlled by DEBUG env var)
|
|
36
|
+
this.debug('called with options %O', options)
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
38
|
+
// Use this.logger for user-facing output
|
|
39
|
+
this.logger.info('Starting operation...')
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
// Command implementation
|
|
42
|
+
}
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
preAction(command) {
|
|
45
|
+
// Optional: runs before action()
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
postAction(command) {
|
|
49
|
+
// Optional: runs after action()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
```
|
|
34
53
|
|
|
35
|
-
|
|
54
|
+
### Inherited Properties
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
The base `Command` class provides these properties to all commands:
|
|
38
57
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
| Property | Type | Description |
|
|
59
|
+
|----------|------|-------------|
|
|
60
|
+
| `this.logger` | Logger | User-facing output with prefix `commands:{name}` |
|
|
61
|
+
| `this.debug` | Function | Developer debugging via `DEBUG` env var |
|
|
62
|
+
| `this.config` | Conf | CLI configuration store |
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
#### Using Logger vs Debug
|
|
44
65
|
|
|
45
|
-
|
|
66
|
+
```javascript
|
|
67
|
+
// Developer debugging - only shown when DEBUG=quire:commands:* is set
|
|
68
|
+
this.debug('processing file: %s', filename)
|
|
69
|
+
this.debug('options: %O', options)
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
// User-facing output - always shown (unless silenced by log level)
|
|
72
|
+
this.logger.info('Building PDF...')
|
|
73
|
+
this.logger.warn('Deprecated option used')
|
|
74
|
+
this.logger.error('Build failed: %s', error.message)
|
|
51
75
|
```
|
|
52
76
|
|
|
53
|
-
###
|
|
77
|
+
### File Organization
|
|
54
78
|
|
|
55
|
-
|
|
79
|
+
For each command, you'll find up to three types of files:
|
|
56
80
|
|
|
57
|
-
```
|
|
58
|
-
|
|
81
|
+
```
|
|
82
|
+
src/commands/
|
|
83
|
+
├── my-command.js # Command implementation
|
|
84
|
+
├── my-command.spec.js # Unit tests (structure, options, definitions)
|
|
85
|
+
└── my-command.test.js # Integration tests (functionality with mocked deps)
|
|
59
86
|
```
|
|
60
87
|
|
|
61
|
-
|
|
88
|
+
## Testing Strategy
|
|
62
89
|
|
|
63
|
-
|
|
90
|
+
The Quire CLI uses a comprehensive three-tier testing strategy to ensure reliable command functionality:
|
|
64
91
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
| Test Type | Pattern | Purpose | Location | Speed |
|
|
93
|
+
|-----------|---------|---------|----------|-------|
|
|
94
|
+
| **Unit** | `*.spec.js` | Command structure, options, definitions | `src/commands/` | Very Fast (seconds) |
|
|
95
|
+
| **Integration** | `*.test.js` | Command functionality with mocked dependencies | `src/commands/` | Fast (seconds) |
|
|
96
|
+
| **E2E** | `*.e2e.js` | Complete workflows with real dependencies | `test/e2e/` | Slow (minutes) |
|
|
68
97
|
|
|
69
|
-
### `
|
|
98
|
+
### Unit Tests (`*.spec.js`)
|
|
70
99
|
|
|
71
|
-
|
|
100
|
+
Unit tests verify the command structure and configuration without executing any logic.
|
|
72
101
|
|
|
73
|
-
|
|
102
|
+
**What they test:**
|
|
103
|
+
- Command is properly defined: name, description, and version
|
|
104
|
+
- Command is properly instantiated
|
|
105
|
+
- Arguments and options are properly defined
|
|
106
|
+
- Method existence (action, preAction)
|
|
107
|
+
- No actual functionality testing
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
**Example:**
|
|
110
|
+
```javascript
|
|
111
|
+
test('build command should have a dry-run option', (t) => {
|
|
112
|
+
const { command } = t.context
|
|
113
|
+
const dryRunOption = command.options.find((opt) => opt.flags.includes('--dry-run'))
|
|
114
|
+
t.truthy(dryRunOption)
|
|
115
|
+
})
|
|
77
116
|
```
|
|
78
117
|
|
|
79
|
-
|
|
118
|
+
### Integration Tests (`*.test.js`)
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
quire new <path>
|
|
83
|
-
```
|
|
120
|
+
Integration tests verify command functionality with all external dependencies mocked.
|
|
84
121
|
|
|
85
|
-
|
|
122
|
+
**What they test:**
|
|
123
|
+
- Command execution flow
|
|
124
|
+
- Interaction with mocked dependencies
|
|
125
|
+
- Option handling
|
|
126
|
+
- Error scenarios
|
|
86
127
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
Test command functionality with mocked dependencies:
|
|
129
|
+
- Uses `esmock` for ES module mocking
|
|
130
|
+
- Uses `memfs` for in-memory file system
|
|
131
|
+
- Uses `sinon` for function stubs
|
|
90
132
|
|
|
91
|
-
|
|
133
|
+
**Example:**
|
|
134
|
+
```javascript
|
|
135
|
+
test('build command should call eleventy CLI with default options', async (t) => {
|
|
136
|
+
const { sandbox, fs } = t.context
|
|
92
137
|
|
|
93
|
-
|
|
138
|
+
const mockEleventyCli = {
|
|
139
|
+
build: sandbox.stub().resolves({ exitCode: 0 })
|
|
140
|
+
}
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
const BuildCommand = await esmock('./build.js', {
|
|
143
|
+
'#lib/11ty/index.js': {
|
|
144
|
+
cli: mockEleventyCli,
|
|
145
|
+
paths: { output: '_site' },
|
|
146
|
+
projectRoot: '/project'
|
|
147
|
+
},
|
|
148
|
+
'fs-extra': fs
|
|
149
|
+
})
|
|
100
150
|
|
|
101
|
-
|
|
151
|
+
const command = new BuildCommand()
|
|
152
|
+
await command.action({ '11ty': 'cli' }, command)
|
|
102
153
|
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
t.true(mockEleventyCli.build.called)
|
|
155
|
+
})
|
|
105
156
|
```
|
|
106
157
|
|
|
107
|
-
|
|
108
|
-
quire new ./blargh --quire latest
|
|
109
|
-
```
|
|
158
|
+
### E2E Tests (`*.e2e.js`)
|
|
110
159
|
|
|
111
|
-
|
|
160
|
+
E2E tests verify complete workflows using real dependencies.
|
|
112
161
|
|
|
113
|
-
|
|
162
|
+
**What they test:**
|
|
163
|
+
- Complete command workflows
|
|
164
|
+
- Cross-platform compatibility
|
|
165
|
+
- Real file system operations
|
|
166
|
+
- Real external processes
|
|
167
|
+
- Actual PDF/EPUB generation
|
|
114
168
|
|
|
115
|
-
|
|
116
|
-
|
|
169
|
+
**Example:**
|
|
170
|
+
```javascript
|
|
171
|
+
test('build command should create _site directory', async (t) => {
|
|
172
|
+
await exec('quire build')
|
|
173
|
+
t.true(fs.existsSync('./_site'))
|
|
174
|
+
})
|
|
117
175
|
```
|
|
118
176
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Preview the Quire publication epub in the default application.
|
|
177
|
+
## Running Tests
|
|
122
178
|
|
|
123
179
|
```sh
|
|
124
|
-
|
|
125
|
-
|
|
180
|
+
# Run all tests (unit + integration + e2e)
|
|
181
|
+
npm test
|
|
126
182
|
|
|
127
|
-
|
|
183
|
+
# Run only unit tests (fast)
|
|
184
|
+
npm run test:unit
|
|
128
185
|
|
|
129
|
-
|
|
186
|
+
# Run only integration tests (fast, mocked)
|
|
187
|
+
npm run test:integration
|
|
130
188
|
|
|
131
|
-
|
|
132
|
-
|
|
189
|
+
# Run only E2E tests (slow, real dependencies)
|
|
190
|
+
npm run test:e2e
|
|
191
|
+
|
|
192
|
+
# Watch mode for development
|
|
193
|
+
npm run test:watch
|
|
194
|
+
|
|
195
|
+
# Generate coverage report
|
|
196
|
+
npm run test:coverage
|
|
133
197
|
```
|
|
134
198
|
|
|
135
|
-
|
|
199
|
+
## Writing Tests
|
|
136
200
|
|
|
137
|
-
|
|
201
|
+
### When to Write Each Type
|
|
138
202
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
203
|
+
✅ **Always write `*.spec.js`** - Unit tests for every command
|
|
204
|
+
✅ **Always write `*.test.js`** - Integration tests for command logic
|
|
205
|
+
⚠️ **Optionally write `*.e2e.js`** - E2E tests for critical workflows
|
|
142
206
|
|
|
143
|
-
###
|
|
207
|
+
### Integration Test Pattern
|
|
144
208
|
|
|
145
|
-
|
|
209
|
+
```javascript
|
|
210
|
+
import test from 'ava'
|
|
211
|
+
import { Volume, createFsFromVolume } from 'memfs'
|
|
212
|
+
import sinon from 'sinon'
|
|
213
|
+
import esmock from 'esmock'
|
|
146
214
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
215
|
+
test.beforeEach((t) => {
|
|
216
|
+
// Create sinon sandbox for mocking
|
|
217
|
+
t.context.sandbox = sinon.createSandbox()
|
|
150
218
|
|
|
151
|
-
|
|
219
|
+
// Create in-memory file system
|
|
220
|
+
t.context.vol = new Volume()
|
|
221
|
+
t.context.fs = createFsFromVolume(t.context.vol)
|
|
152
222
|
|
|
153
|
-
|
|
223
|
+
// Setup mock directory structure
|
|
224
|
+
t.context.vol.fromJSON({
|
|
225
|
+
'/project/content/_data/config.yaml': 'title: Test Project',
|
|
226
|
+
'/project/package.json': JSON.stringify({ name: 'test-project' })
|
|
227
|
+
})
|
|
154
228
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```
|
|
229
|
+
t.context.projectRoot = '/project'
|
|
230
|
+
})
|
|
158
231
|
|
|
159
|
-
|
|
232
|
+
test.afterEach.always((t) => {
|
|
233
|
+
// Restore all mocks
|
|
234
|
+
t.context.sandbox.restore()
|
|
160
235
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
// Clear in-memory file system
|
|
237
|
+
t.context.vol.reset()
|
|
238
|
+
})
|
|
164
239
|
|
|
165
|
-
|
|
240
|
+
test('command should perform expected operation', async (t) => {
|
|
241
|
+
const { sandbox, fs } = t.context
|
|
166
242
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
```
|
|
243
|
+
// Mock dependencies
|
|
244
|
+
const mockDependency = sandbox.stub().resolves()
|
|
170
245
|
|
|
171
|
-
|
|
246
|
+
// Use esmock to load command with mocked dependencies
|
|
247
|
+
const MyCommand = await esmock('./mycommand.js', {
|
|
248
|
+
'#lib/dependency/index.js': mockDependency,
|
|
249
|
+
'fs-extra': fs
|
|
250
|
+
})
|
|
172
251
|
|
|
173
|
-
|
|
174
|
-
|
|
252
|
+
const command = new MyCommand()
|
|
253
|
+
await command.action({}, command)
|
|
254
|
+
|
|
255
|
+
t.true(mockDependency.called)
|
|
256
|
+
})
|
|
175
257
|
```
|
|
176
258
|
|
|
177
|
-
|
|
259
|
+
## Testing Dependencies
|
|
178
260
|
|
|
179
|
-
|
|
261
|
+
All testing dependencies are managed in [package.json](../../package.json):
|
|
180
262
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
263
|
+
- **ava** - Test runner with ES module support
|
|
264
|
+
- **esmock** - ES module mocking for imports
|
|
265
|
+
- **memfs** - In-memory file system for fast, isolated tests
|
|
266
|
+
- **sinon** - Stubbing and mocking for function calls
|
|
184
267
|
|
|
185
|
-
|
|
268
|
+
## Test Quality Standards
|
|
186
269
|
|
|
187
|
-
|
|
270
|
+
- Each command must have integration test scenarios
|
|
271
|
+
- Critical workflows must have end-to-end tests
|
|
272
|
+
- All error paths must be tested
|
|
273
|
+
- Test execution time should be < 5 minutes total (with mocking: < 30 seconds)
|
|
274
|
+
- Code coverage target: 80% for integration paths
|
|
188
275
|
|
|
189
|
-
|
|
190
|
-
quire version prune
|
|
191
|
-
```
|
|
276
|
+
## Creating a New Command
|
|
192
277
|
|
|
193
|
-
|
|
278
|
+
1. **Create the command file** (`src/commands/my-command.js`)
|
|
279
|
+
- Extend the `Command` class
|
|
280
|
+
- Define static `definition` object
|
|
281
|
+
- Implement `action()` method
|
|
194
282
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
283
|
+
2. **Create integration tests** (`src/commands/my-command.test.js`)
|
|
284
|
+
- Test main functionality with mocked dependencies
|
|
285
|
+
- Test error handling
|
|
286
|
+
- Test option passing
|
|
198
287
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
288
|
+
3. **Create unit tests** (optional, `src/commands/my-command.spec.js`)
|
|
289
|
+
- Validate command structure and configuration
|
|
290
|
+
|
|
291
|
+
4. **Create e2e tests** (optional, `test/e2e/my-command.e2e.js`)
|
|
292
|
+
- Test critical workflows with real dependencies
|