Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/tests/smoke/use/brand.test.ts
12925 views
1
import { testQuartoCmd, ExecuteOutput, Verify } from "../../test.ts";
2
import { fileExists, folderExists, noErrorsOrWarnings, printsMessage } from "../../verify.ts";
3
import { join, fromFileUrl, dirname } from "../../../src/deno_ral/path.ts";
4
import { ensureDirSync, existsSync } from "../../../src/deno_ral/fs.ts";
5
import { pathWithForwardSlashes } from "../../../src/core/path.ts";
6
7
// Helper to verify files appear in the correct output sections
8
function filesInSections(
9
expected: { overwrite?: string[]; create?: string[]; remove?: string[] },
10
dryRun: boolean
11
): Verify {
12
return {
13
name: "files in correct sections",
14
verify: (outputs: ExecuteOutput[]) => {
15
const overwriteHeader = dryRun ? "Would overwrite:" : "Overwritten:";
16
const createHeader = dryRun ? "Would create:" : "Created:";
17
const removeHeader = dryRun ? "Would remove:" : "Removed:";
18
19
const found: { overwrite: string[]; create: string[]; remove: string[] } = {
20
overwrite: [],
21
create: [],
22
remove: [],
23
};
24
let currentSection: "overwrite" | "create" | "remove" | null = null;
25
26
for (const output of outputs) {
27
const line = output.msg;
28
if (line.includes(overwriteHeader)) {
29
currentSection = "overwrite";
30
} else if (line.includes(createHeader)) {
31
currentSection = "create";
32
} else if (line.includes(removeHeader)) {
33
currentSection = "remove";
34
} else if (currentSection && line.trim().startsWith("- ")) {
35
const filename = line.trim().slice(2); // remove "- "
36
// Normalize path separators for cross-platform compatibility
37
found[currentSection].push(pathWithForwardSlashes(filename));
38
}
39
}
40
41
// Verify expected files are in correct sections
42
for (const file of expected.overwrite ?? []) {
43
if (!found.overwrite.includes(pathWithForwardSlashes(file))) {
44
throw new Error(`Expected ${file} in overwrite section, found: [${found.overwrite.join(", ")}]`);
45
}
46
}
47
for (const file of expected.create ?? []) {
48
if (!found.create.includes(pathWithForwardSlashes(file))) {
49
throw new Error(`Expected ${file} in create section, found: [${found.create.join(", ")}]`);
50
}
51
}
52
for (const file of expected.remove ?? []) {
53
if (!found.remove.includes(pathWithForwardSlashes(file))) {
54
throw new Error(`Expected ${file} in remove section, found: [${found.remove.join(", ")}]`);
55
}
56
}
57
return Promise.resolve();
58
}
59
};
60
}
61
62
const tempDir = Deno.makeTempDirSync();
63
const testDir = dirname(fromFileUrl(import.meta.url));
64
const fixtureDir = join(testDir, "..", "use-brand");
65
66
// Scenario 1: Basic brand installation
67
const basicDir = join(tempDir, "basic");
68
ensureDirSync(basicDir);
69
testQuartoCmd(
70
"use",
71
["brand", join(fixtureDir, "basic-brand"), "--force"],
72
[
73
noErrorsOrWarnings,
74
folderExists(join(basicDir, "_brand")),
75
fileExists(join(basicDir, "_brand", "_brand.yml")),
76
fileExists(join(basicDir, "_brand", "logo.png")),
77
// Font file referenced in typography.fonts should be copied
78
folderExists(join(basicDir, "_brand", "fonts")),
79
fileExists(join(basicDir, "_brand", "fonts", "custom-font.woff2")),
80
// README.md is NOT referenced in _brand.yml - should NOT be copied
81
{
82
name: "README.md should not be copied (unreferenced)",
83
verify: () => {
84
if (existsSync(join(basicDir, "_brand", "README.md"))) {
85
throw new Error("README.md should not be copied - it is not referenced in _brand.yml");
86
}
87
return Promise.resolve();
88
}
89
},
90
],
91
{
92
setup: () => {
93
Deno.writeTextFileSync(join(basicDir, "_quarto.yml"), "project:\n type: default\n");
94
return Promise.resolve();
95
},
96
cwd: () => basicDir,
97
teardown: () => {
98
try { Deno.removeSync(basicDir, { recursive: true }); } catch { /* ignore */ }
99
return Promise.resolve();
100
}
101
},
102
"quarto use brand - basic installation"
103
);
104
105
// Scenario 2: Dry-run mode
106
const dryRunDir = join(tempDir, "dry-run");
107
ensureDirSync(dryRunDir);
108
testQuartoCmd(
109
"use",
110
["brand", join(fixtureDir, "basic-brand"), "--dry-run"],
111
[
112
noErrorsOrWarnings,
113
printsMessage({ level: "INFO", regex: /Would create directory/ }),
114
filesInSections({ create: ["_brand.yml", "logo.png", "fonts/custom-font.woff2"] }, true),
115
{
116
name: "_brand directory should not exist in dry-run mode",
117
verify: () => {
118
const brandDir = join(dryRunDir, "_brand");
119
if (existsSync(brandDir)) {
120
throw new Error("_brand directory should not exist in dry-run mode");
121
}
122
return Promise.resolve();
123
}
124
},
125
// README.md should NOT appear in dry-run output (unreferenced)
126
{
127
name: "README.md should not be listed in dry-run output (unreferenced)",
128
verify: (outputs: ExecuteOutput[]) => {
129
for (const output of outputs) {
130
if (output.msg.includes("README.md")) {
131
throw new Error("README.md should not appear in dry-run output - it is not referenced in _brand.yml");
132
}
133
}
134
return Promise.resolve();
135
}
136
},
137
],
138
{
139
setup: () => {
140
Deno.writeTextFileSync(join(dryRunDir, "_quarto.yml"), "project:\n type: default\n");
141
return Promise.resolve();
142
},
143
cwd: () => dryRunDir,
144
teardown: () => {
145
try { Deno.removeSync(dryRunDir, { recursive: true }); } catch { /* ignore */ }
146
return Promise.resolve();
147
}
148
},
149
"quarto use brand - dry-run mode"
150
);
151
152
// Scenario 3: Force mode - overwrites existing, creates new, removes extra
153
const forceOverwriteDir = join(tempDir, "force-overwrite");
154
ensureDirSync(forceOverwriteDir);
155
testQuartoCmd(
156
"use",
157
["brand", join(fixtureDir, "basic-brand"), "--force"],
158
[
159
noErrorsOrWarnings,
160
// _brand.yml should be overwritten (exists in both)
161
{
162
name: "_brand.yml should be overwritten with new content",
163
verify: () => {
164
const content = Deno.readTextFileSync(join(forceOverwriteDir, "_brand", "_brand.yml"));
165
if (content.includes("Old Brand")) {
166
throw new Error("_brand.yml should have been overwritten");
167
}
168
if (!content.includes("Basic Test Brand")) {
169
throw new Error("_brand.yml should contain new brand content");
170
}
171
return Promise.resolve();
172
}
173
},
174
// logo.png should be created (not in target originally)
175
fileExists(join(forceOverwriteDir, "_brand", "logo.png")),
176
// unrelated.txt should be removed (not in source)
177
{
178
name: "unrelated.txt should be removed",
179
verify: () => {
180
if (existsSync(join(forceOverwriteDir, "_brand", "unrelated.txt"))) {
181
throw new Error("unrelated.txt should have been removed");
182
}
183
return Promise.resolve();
184
}
185
},
186
// Verify output sections
187
filesInSections({ overwrite: ["_brand.yml"], create: ["logo.png"], remove: ["unrelated.txt"] }, false),
188
],
189
{
190
setup: () => {
191
Deno.writeTextFileSync(join(forceOverwriteDir, "_quarto.yml"), "project:\n type: default\n");
192
// Create existing _brand directory with files
193
const brandDir = join(forceOverwriteDir, "_brand");
194
ensureDirSync(brandDir);
195
// This file exists in source - should be overwritten
196
Deno.writeTextFileSync(join(brandDir, "_brand.yml"), "meta:\n name: Old Brand\n");
197
// This file does NOT exist in source - should be preserved
198
Deno.writeTextFileSync(join(brandDir, "unrelated.txt"), "keep me");
199
return Promise.resolve();
200
},
201
cwd: () => forceOverwriteDir,
202
teardown: () => {
203
try { Deno.removeSync(forceOverwriteDir, { recursive: true }); } catch { /* ignore */ }
204
return Promise.resolve();
205
}
206
},
207
"quarto use brand - force overwrites existing, creates new, removes extra"
208
);
209
210
// Scenario 4: Dry-run reports "Would overwrite" vs "Would create" vs "Would remove" correctly
211
const dryRunOverwriteDir = join(tempDir, "dry-run-overwrite");
212
ensureDirSync(dryRunOverwriteDir);
213
testQuartoCmd(
214
"use",
215
["brand", join(fixtureDir, "basic-brand"), "--dry-run"],
216
[
217
noErrorsOrWarnings,
218
// _brand.yml exists - should be in overwrite section
219
// logo.png doesn't exist - should be in create section
220
// extra.txt exists only in target - should be in remove section
221
filesInSections({
222
overwrite: ["_brand.yml"],
223
create: ["logo.png"],
224
remove: ["extra.txt"]
225
}, true),
226
// Verify _brand.yml was NOT modified
227
{
228
name: "_brand.yml should not be modified in dry-run",
229
verify: () => {
230
const content = Deno.readTextFileSync(join(dryRunOverwriteDir, "_brand", "_brand.yml"));
231
if (!content.includes("Old Brand")) {
232
throw new Error("_brand.yml should not be modified in dry-run mode");
233
}
234
return Promise.resolve();
235
}
236
},
237
// Verify logo.png was NOT created
238
{
239
name: "logo.png should not be created in dry-run",
240
verify: () => {
241
if (existsSync(join(dryRunOverwriteDir, "_brand", "logo.png"))) {
242
throw new Error("logo.png should not be created in dry-run mode");
243
}
244
return Promise.resolve();
245
}
246
},
247
// Verify extra.txt was NOT removed
248
{
249
name: "extra.txt should not be removed in dry-run",
250
verify: () => {
251
if (!existsSync(join(dryRunOverwriteDir, "_brand", "extra.txt"))) {
252
throw new Error("extra.txt should not be removed in dry-run mode");
253
}
254
return Promise.resolve();
255
}
256
},
257
],
258
{
259
setup: () => {
260
Deno.writeTextFileSync(join(dryRunOverwriteDir, "_quarto.yml"), "project:\n type: default\n");
261
// Create existing _brand directory with _brand.yml and extra.txt (not logo.png)
262
const brandDir = join(dryRunOverwriteDir, "_brand");
263
ensureDirSync(brandDir);
264
Deno.writeTextFileSync(join(brandDir, "_brand.yml"), "meta:\n name: Old Brand\n");
265
Deno.writeTextFileSync(join(brandDir, "extra.txt"), "extra file not in source");
266
return Promise.resolve();
267
},
268
cwd: () => dryRunOverwriteDir,
269
teardown: () => {
270
try { Deno.removeSync(dryRunOverwriteDir, { recursive: true }); } catch { /* ignore */ }
271
return Promise.resolve();
272
}
273
},
274
"quarto use brand - dry-run reports overwrite vs create vs remove correctly"
275
);
276
277
// Scenario 5: Error - force and dry-run together
278
const errorFlagDir = join(tempDir, "error-flags");
279
ensureDirSync(errorFlagDir);
280
testQuartoCmd(
281
"use",
282
["brand", join(fixtureDir, "basic-brand"), "--force", "--dry-run"],
283
[
284
printsMessage({ level: "ERROR", regex: /Cannot use --force and --dry-run together/ }),
285
],
286
{
287
setup: () => {
288
Deno.writeTextFileSync(join(errorFlagDir, "_quarto.yml"), "project:\n type: default\n");
289
return Promise.resolve();
290
},
291
cwd: () => errorFlagDir,
292
teardown: () => {
293
try { Deno.removeSync(errorFlagDir, { recursive: true }); } catch { /* ignore */ }
294
return Promise.resolve();
295
}
296
},
297
"quarto use brand - error on --force --dry-run"
298
);
299
300
// Scenario 6: Multi-file brand installation
301
const multiFileDir = join(tempDir, "multi-file");
302
ensureDirSync(multiFileDir);
303
testQuartoCmd(
304
"use",
305
["brand", join(fixtureDir, "multi-file-brand"), "--force"],
306
[
307
noErrorsOrWarnings,
308
folderExists(join(multiFileDir, "_brand")),
309
fileExists(join(multiFileDir, "_brand", "_brand.yml")),
310
fileExists(join(multiFileDir, "_brand", "logo.png")),
311
fileExists(join(multiFileDir, "_brand", "favicon.png")),
312
// Font files referenced in typography.fonts should be copied
313
folderExists(join(multiFileDir, "_brand", "fonts")),
314
fileExists(join(multiFileDir, "_brand", "fonts", "brand-regular.woff2")),
315
fileExists(join(multiFileDir, "_brand", "fonts", "brand-bold.woff2")),
316
// unused-styles.css is NOT referenced in _brand.yml - should NOT be copied
317
{
318
name: "unused-styles.css should not be copied (unreferenced)",
319
verify: () => {
320
if (existsSync(join(multiFileDir, "_brand", "unused-styles.css"))) {
321
throw new Error("unused-styles.css should not be copied - it is not referenced in _brand.yml");
322
}
323
return Promise.resolve();
324
}
325
},
326
// fonts/unused-italic.woff2 is NOT referenced in _brand.yml - should NOT be copied
327
{
328
name: "fonts/unused-italic.woff2 should not be copied (unreferenced)",
329
verify: () => {
330
if (existsSync(join(multiFileDir, "_brand", "fonts", "unused-italic.woff2"))) {
331
throw new Error("fonts/unused-italic.woff2 should not be copied - it is not referenced in _brand.yml");
332
}
333
return Promise.resolve();
334
}
335
},
336
],
337
{
338
setup: () => {
339
Deno.writeTextFileSync(join(multiFileDir, "_quarto.yml"), "project:\n type: default\n");
340
return Promise.resolve();
341
},
342
cwd: () => multiFileDir,
343
teardown: () => {
344
try { Deno.removeSync(multiFileDir, { recursive: true }); } catch { /* ignore */ }
345
return Promise.resolve();
346
}
347
},
348
"quarto use brand - multi-file installation"
349
);
350
351
// Scenario 7: Nested directory structure preserved
352
const nestedDir = join(tempDir, "nested");
353
ensureDirSync(nestedDir);
354
testQuartoCmd(
355
"use",
356
["brand", join(fixtureDir, "nested-brand"), "--force"],
357
[
358
noErrorsOrWarnings,
359
folderExists(join(nestedDir, "_brand")),
360
fileExists(join(nestedDir, "_brand", "_brand.yml")),
361
folderExists(join(nestedDir, "_brand", "images")),
362
fileExists(join(nestedDir, "_brand", "images", "logo.png")),
363
fileExists(join(nestedDir, "_brand", "images", "header.png")),
364
// notes.txt is NOT referenced in _brand.yml - should NOT be copied
365
{
366
name: "notes.txt should not be copied (unreferenced)",
367
verify: () => {
368
if (existsSync(join(nestedDir, "_brand", "notes.txt"))) {
369
throw new Error("notes.txt should not be copied - it is not referenced in _brand.yml");
370
}
371
return Promise.resolve();
372
}
373
},
374
// images/extra-icon.png is NOT referenced in _brand.yml - should NOT be copied
375
{
376
name: "images/extra-icon.png should not be copied (unreferenced)",
377
verify: () => {
378
if (existsSync(join(nestedDir, "_brand", "images", "extra-icon.png"))) {
379
throw new Error("images/extra-icon.png should not be copied - it is not referenced in _brand.yml");
380
}
381
return Promise.resolve();
382
}
383
},
384
],
385
{
386
setup: () => {
387
Deno.writeTextFileSync(join(nestedDir, "_quarto.yml"), "project:\n type: default\n");
388
return Promise.resolve();
389
},
390
cwd: () => nestedDir,
391
teardown: () => {
392
try { Deno.removeSync(nestedDir, { recursive: true }); } catch { /* ignore */ }
393
return Promise.resolve();
394
}
395
},
396
"quarto use brand - nested directory structure"
397
);
398
399
// Scenario 8: Single-file mode (no _quarto.yml) - should work, using current directory
400
const noProjectDir = join(tempDir, "no-project");
401
ensureDirSync(noProjectDir);
402
testQuartoCmd(
403
"use",
404
["brand", join(fixtureDir, "basic-brand"), "--force"],
405
[
406
noErrorsOrWarnings,
407
// Should create _brand/ in the current directory even without _quarto.yml
408
folderExists(join(noProjectDir, "_brand")),
409
fileExists(join(noProjectDir, "_brand", "_brand.yml")),
410
fileExists(join(noProjectDir, "_brand", "logo.png")),
411
],
412
{
413
setup: () => {
414
// No _quarto.yml created - single-file mode should work
415
return Promise.resolve();
416
},
417
cwd: () => noProjectDir,
418
teardown: () => {
419
try { Deno.removeSync(noProjectDir, { recursive: true }); } catch { /* ignore */ }
420
return Promise.resolve();
421
}
422
},
423
"quarto use brand - single-file mode (no _quarto.yml)"
424
);
425
426
// Scenario 9: Nested directory - overwrite files in subdirectories, remove extra
427
const nestedOverwriteDir = join(tempDir, "nested-overwrite");
428
ensureDirSync(nestedOverwriteDir);
429
testQuartoCmd(
430
"use",
431
["brand", join(fixtureDir, "nested-brand"), "--force"],
432
[
433
noErrorsOrWarnings,
434
// images/logo.png should be overwritten (exists in both)
435
{
436
name: "images/logo.png should be overwritten",
437
verify: () => {
438
const stats = Deno.statSync(join(nestedOverwriteDir, "_brand", "images", "logo.png"));
439
// Original was 10 bytes ("old logo\n"), new one is 1862 bytes
440
if (stats.size < 100) {
441
throw new Error("images/logo.png should have been overwritten with larger file");
442
}
443
return Promise.resolve();
444
}
445
},
446
// images/header.png should be created (not in target originally)
447
fileExists(join(nestedOverwriteDir, "_brand", "images", "header.png")),
448
// images/unrelated.png should be removed (not in source)
449
{
450
name: "images/unrelated.png should be removed",
451
verify: () => {
452
if (existsSync(join(nestedOverwriteDir, "_brand", "images", "unrelated.png"))) {
453
throw new Error("images/unrelated.png should have been removed");
454
}
455
return Promise.resolve();
456
}
457
},
458
// Verify output sections (_brand.yml is created since not in target setup)
459
filesInSections({
460
overwrite: ["images/logo.png"],
461
create: ["_brand.yml", "images/header.png"],
462
remove: ["images/unrelated.png"]
463
}, false),
464
],
465
{
466
setup: () => {
467
Deno.writeTextFileSync(join(nestedOverwriteDir, "_quarto.yml"), "project:\n type: default\n");
468
// Create existing _brand/images directory with files
469
const imagesDir = join(nestedOverwriteDir, "_brand", "images");
470
ensureDirSync(imagesDir);
471
// This file exists in source - should be overwritten
472
Deno.writeTextFileSync(join(imagesDir, "logo.png"), "old logo\n");
473
// This file does NOT exist in source - should be preserved
474
Deno.writeTextFileSync(join(imagesDir, "unrelated.png"), "keep me nested");
475
return Promise.resolve();
476
},
477
cwd: () => nestedOverwriteDir,
478
teardown: () => {
479
try { Deno.removeSync(nestedOverwriteDir, { recursive: true }); } catch { /* ignore */ }
480
return Promise.resolve();
481
}
482
},
483
"quarto use brand - nested overwrite, create, remove in subdirectories"
484
);
485
486
// Scenario 10: Dry-run with nested directories - reports correctly
487
const dryRunNestedDir = join(tempDir, "dry-run-nested");
488
ensureDirSync(dryRunNestedDir);
489
testQuartoCmd(
490
"use",
491
["brand", join(fixtureDir, "nested-brand"), "--dry-run"],
492
[
493
noErrorsOrWarnings,
494
// images/logo.png and _brand.yml exist - should be in overwrite section
495
// images/header.png doesn't exist - should be in create section
496
filesInSections({
497
overwrite: ["_brand.yml", "images/logo.png"],
498
create: ["images/header.png"]
499
}, true),
500
// Verify images/logo.png was NOT modified
501
{
502
name: "images/logo.png should not be modified in dry-run",
503
verify: () => {
504
const content = Deno.readTextFileSync(join(dryRunNestedDir, "_brand", "images", "logo.png"));
505
if (content !== "old logo\n") {
506
throw new Error("images/logo.png should not be modified in dry-run mode");
507
}
508
return Promise.resolve();
509
}
510
},
511
// Verify images/header.png was NOT created
512
{
513
name: "images/header.png should not be created in dry-run",
514
verify: () => {
515
if (existsSync(join(dryRunNestedDir, "_brand", "images", "header.png"))) {
516
throw new Error("images/header.png should not be created in dry-run mode");
517
}
518
return Promise.resolve();
519
}
520
},
521
],
522
{
523
setup: () => {
524
Deno.writeTextFileSync(join(dryRunNestedDir, "_quarto.yml"), "project:\n type: default\n");
525
// Create existing _brand/images directory with only logo.png (not header.png)
526
const imagesDir = join(dryRunNestedDir, "_brand", "images");
527
ensureDirSync(imagesDir);
528
Deno.writeTextFileSync(join(imagesDir, "logo.png"), "old logo\n");
529
// Also create _brand.yml so we're only testing nested behavior
530
Deno.writeTextFileSync(join(dryRunNestedDir, "_brand", "_brand.yml"), "meta:\n name: Old\n");
531
return Promise.resolve();
532
},
533
cwd: () => dryRunNestedDir,
534
teardown: () => {
535
try { Deno.removeSync(dryRunNestedDir, { recursive: true }); } catch { /* ignore */ }
536
return Promise.resolve();
537
}
538
},
539
"quarto use brand - dry-run reports nested overwrite vs create correctly"
540
);
541
542
// Scenario 11: Nested directory created when doesn't exist
543
const nestedNewSubdirDir = join(tempDir, "nested-new-subdir");
544
ensureDirSync(nestedNewSubdirDir);
545
testQuartoCmd(
546
"use",
547
["brand", join(fixtureDir, "nested-brand"), "--force"],
548
[
549
noErrorsOrWarnings,
550
// _brand/ exists but images/ doesn't - should be created
551
folderExists(join(nestedNewSubdirDir, "_brand", "images")),
552
fileExists(join(nestedNewSubdirDir, "_brand", "images", "logo.png")),
553
fileExists(join(nestedNewSubdirDir, "_brand", "images", "header.png")),
554
// existing file at root should be overwritten
555
{
556
name: "_brand.yml should be overwritten",
557
verify: () => {
558
const content = Deno.readTextFileSync(join(nestedNewSubdirDir, "_brand", "_brand.yml"));
559
if (content.includes("Old Brand")) {
560
throw new Error("_brand.yml should have been overwritten");
561
}
562
return Promise.resolve();
563
}
564
},
565
],
566
{
567
setup: () => {
568
Deno.writeTextFileSync(join(nestedNewSubdirDir, "_quarto.yml"), "project:\n type: default\n");
569
// Create _brand/ but NOT images/ subdirectory
570
const brandDir = join(nestedNewSubdirDir, "_brand");
571
ensureDirSync(brandDir);
572
Deno.writeTextFileSync(join(brandDir, "_brand.yml"), "meta:\n name: Old Brand\n");
573
return Promise.resolve();
574
},
575
cwd: () => nestedNewSubdirDir,
576
teardown: () => {
577
try { Deno.removeSync(nestedNewSubdirDir, { recursive: true }); } catch { /* ignore */ }
578
return Promise.resolve();
579
}
580
},
581
"quarto use brand - creates nested subdirectory when _brand exists but subdir doesn't"
582
);
583
584
// Scenario 12: Dry-run reports new subdirectory creation
585
const dryRunNewSubdirDir = join(tempDir, "dry-run-new-subdir");
586
ensureDirSync(dryRunNewSubdirDir);
587
testQuartoCmd(
588
"use",
589
["brand", join(fixtureDir, "nested-brand"), "--dry-run"],
590
[
591
noErrorsOrWarnings,
592
// Should NOT report "Would create directory" for _brand/ (already exists)
593
printsMessage({ level: "INFO", regex: /Would create directory/, negate: true }),
594
// _brand.yml exists - should be in overwrite section
595
// images/* files don't exist - should be in create section
596
filesInSections({
597
overwrite: ["_brand.yml"],
598
create: ["images/logo.png", "images/header.png"]
599
}, true),
600
// Verify images/ directory was NOT created
601
{
602
name: "images/ directory should not be created in dry-run",
603
verify: () => {
604
if (existsSync(join(dryRunNewSubdirDir, "_brand", "images"))) {
605
throw new Error("images/ directory should not be created in dry-run mode");
606
}
607
return Promise.resolve();
608
}
609
},
610
],
611
{
612
setup: () => {
613
Deno.writeTextFileSync(join(dryRunNewSubdirDir, "_quarto.yml"), "project:\n type: default\n");
614
// Create _brand/ but NOT images/ subdirectory
615
const brandDir = join(dryRunNewSubdirDir, "_brand");
616
ensureDirSync(brandDir);
617
Deno.writeTextFileSync(join(brandDir, "_brand.yml"), "meta:\n name: Old\n");
618
return Promise.resolve();
619
},
620
cwd: () => dryRunNewSubdirDir,
621
teardown: () => {
622
try { Deno.removeSync(dryRunNewSubdirDir, { recursive: true }); } catch { /* ignore */ }
623
return Promise.resolve();
624
}
625
},
626
"quarto use brand - dry-run when _brand exists but nested subdir doesn't"
627
);
628
629
// Scenario 13: Empty directories are cleaned up after file removal
630
const emptyDirCleanupDir = join(tempDir, "empty-dir-cleanup");
631
ensureDirSync(emptyDirCleanupDir);
632
testQuartoCmd(
633
"use",
634
["brand", join(fixtureDir, "basic-brand"), "--force"],
635
[
636
noErrorsOrWarnings,
637
// extras/orphan.txt should be removed
638
{
639
name: "extras/orphan.txt should be removed",
640
verify: () => {
641
if (existsSync(join(emptyDirCleanupDir, "_brand", "extras", "orphan.txt"))) {
642
throw new Error("extras/orphan.txt should have been removed");
643
}
644
return Promise.resolve();
645
}
646
},
647
// extras/ directory should be cleaned up (was empty after removal)
648
{
649
name: "extras/ directory should be cleaned up",
650
verify: () => {
651
if (existsSync(join(emptyDirCleanupDir, "_brand", "extras"))) {
652
throw new Error("extras/ directory should have been cleaned up");
653
}
654
return Promise.resolve();
655
}
656
},
657
// Verify output shows removal
658
filesInSections({ remove: ["extras/orphan.txt"] }, false),
659
],
660
{
661
setup: () => {
662
Deno.writeTextFileSync(join(emptyDirCleanupDir, "_quarto.yml"), "project:\n type: default\n");
663
// Create _brand/extras/ with a file not in source
664
const extrasDir = join(emptyDirCleanupDir, "_brand", "extras");
665
ensureDirSync(extrasDir);
666
Deno.writeTextFileSync(join(extrasDir, "orphan.txt"), "this file will be removed");
667
return Promise.resolve();
668
},
669
cwd: () => emptyDirCleanupDir,
670
teardown: () => {
671
try { Deno.removeSync(emptyDirCleanupDir, { recursive: true }); } catch { /* ignore */ }
672
return Promise.resolve();
673
}
674
},
675
"quarto use brand - empty directories cleaned up after file removal"
676
);
677
678
// Scenario 14: Deeply nested directories are recursively cleaned up
679
const deepNestedCleanupDir = join(tempDir, "deep-nested-cleanup");
680
ensureDirSync(deepNestedCleanupDir);
681
testQuartoCmd(
682
"use",
683
["brand", join(fixtureDir, "basic-brand"), "--force"],
684
[
685
noErrorsOrWarnings,
686
// deep/nested/path/orphan.txt should be removed
687
{
688
name: "deep/nested/path/orphan.txt should be removed",
689
verify: () => {
690
if (existsSync(join(deepNestedCleanupDir, "_brand", "deep", "nested", "path", "orphan.txt"))) {
691
throw new Error("deep/nested/path/orphan.txt should have been removed");
692
}
693
return Promise.resolve();
694
}
695
},
696
// All empty parent directories should be cleaned up recursively
697
{
698
name: "deep/ directory tree should be fully cleaned up",
699
verify: () => {
700
if (existsSync(join(deepNestedCleanupDir, "_brand", "deep"))) {
701
throw new Error("deep/ directory should have been cleaned up recursively");
702
}
703
return Promise.resolve();
704
}
705
},
706
// Verify output shows removal with full path
707
filesInSections({ remove: ["deep/nested/path/orphan.txt"] }, false),
708
],
709
{
710
setup: () => {
711
Deno.writeTextFileSync(join(deepNestedCleanupDir, "_quarto.yml"), "project:\n type: default\n");
712
// Create _brand/deep/nested/path/ with a file not in source
713
const deepDir = join(deepNestedCleanupDir, "_brand", "deep", "nested", "path");
714
ensureDirSync(deepDir);
715
Deno.writeTextFileSync(join(deepDir, "orphan.txt"), "deeply nested orphan");
716
return Promise.resolve();
717
},
718
cwd: () => deepNestedCleanupDir,
719
teardown: () => {
720
try { Deno.removeSync(deepNestedCleanupDir, { recursive: true }); } catch { /* ignore */ }
721
return Promise.resolve();
722
}
723
},
724
"quarto use brand - deeply nested directories recursively cleaned up"
725
);
726
727
// Scenario 15: Brand extension - basic installation
728
// Tests that brand extensions are detected and the brand file is renamed to _brand.yml
729
const brandExtDir = join(tempDir, "brand-ext");
730
ensureDirSync(brandExtDir);
731
testQuartoCmd(
732
"use",
733
["brand", join(fixtureDir, "brand-extension"), "--force"],
734
[
735
noErrorsOrWarnings,
736
folderExists(join(brandExtDir, "_brand")),
737
// brand.yml should be renamed to _brand.yml
738
fileExists(join(brandExtDir, "_brand", "_brand.yml")),
739
// logo.png should be copied
740
fileExists(join(brandExtDir, "_brand", "logo.png")),
741
// _extension.yml should NOT be copied
742
{
743
name: "_extension.yml should not be copied",
744
verify: () => {
745
if (existsSync(join(brandExtDir, "_brand", "_extension.yml"))) {
746
throw new Error("_extension.yml should not be copied from brand extension");
747
}
748
return Promise.resolve();
749
}
750
},
751
// Verify the content is correct (from brand.yml, not some other file)
752
{
753
name: "_brand.yml should contain brand extension content",
754
verify: () => {
755
const content = Deno.readTextFileSync(join(brandExtDir, "_brand", "_brand.yml"));
756
if (!content.includes("Test Brand Extension")) {
757
throw new Error("_brand.yml should contain content from brand.yml");
758
}
759
return Promise.resolve();
760
}
761
},
762
// template.html is NOT referenced in brand.yml - should NOT be copied
763
{
764
name: "template.html should not be copied (unreferenced)",
765
verify: () => {
766
if (existsSync(join(brandExtDir, "_brand", "template.html"))) {
767
throw new Error("template.html should not be copied - it is not referenced in brand.yml");
768
}
769
return Promise.resolve();
770
}
771
},
772
],
773
{
774
setup: () => {
775
Deno.writeTextFileSync(join(brandExtDir, "_quarto.yml"), "project:\n type: default\n");
776
return Promise.resolve();
777
},
778
cwd: () => brandExtDir,
779
teardown: () => {
780
try { Deno.removeSync(brandExtDir, { recursive: true }); } catch { /* ignore */ }
781
return Promise.resolve();
782
}
783
},
784
"quarto use brand - brand extension installation"
785
);
786
787
// Scenario 16: Brand extension - dry-run shows correct file names
788
const brandExtDryRunDir = join(tempDir, "brand-ext-dry-run");
789
ensureDirSync(brandExtDryRunDir);
790
testQuartoCmd(
791
"use",
792
["brand", join(fixtureDir, "brand-extension"), "--dry-run"],
793
[
794
noErrorsOrWarnings,
795
// Should show _brand.yml (renamed from brand.yml), not brand.yml
796
filesInSections({ create: ["_brand.yml", "logo.png"] }, true),
797
// _brand directory should not exist in dry-run mode
798
{
799
name: "_brand directory should not exist in dry-run mode",
800
verify: () => {
801
if (existsSync(join(brandExtDryRunDir, "_brand"))) {
802
throw new Error("_brand directory should not exist in dry-run mode");
803
}
804
return Promise.resolve();
805
}
806
}
807
],
808
{
809
setup: () => {
810
Deno.writeTextFileSync(join(brandExtDryRunDir, "_quarto.yml"), "project:\n type: default\n");
811
return Promise.resolve();
812
},
813
cwd: () => brandExtDryRunDir,
814
teardown: () => {
815
try { Deno.removeSync(brandExtDryRunDir, { recursive: true }); } catch { /* ignore */ }
816
return Promise.resolve();
817
}
818
},
819
"quarto use brand - brand extension dry-run shows renamed file"
820
);
821
822
// Scenario 17: Brand extension with brand file in subdirectory
823
// Tests that brand path in _extension.yml can be a relative path (e.g., subdir/brand.yml)
824
// and that referenced files are resolved relative to the brand file's directory
825
const brandExtSubdirDir = join(tempDir, "brand-ext-subdir");
826
ensureDirSync(brandExtSubdirDir);
827
testQuartoCmd(
828
"use",
829
["brand", join(fixtureDir, "brand-extension-subdir"), "--force"],
830
[
831
noErrorsOrWarnings,
832
folderExists(join(brandExtSubdirDir, "_brand")),
833
// subdir/brand.yml should be renamed to _brand.yml
834
fileExists(join(brandExtSubdirDir, "_brand", "_brand.yml")),
835
// logo.png (referenced as logo.png in subdir/brand.yml) should be copied
836
// The logo is at subdir/logo.png relative to extension dir
837
fileExists(join(brandExtSubdirDir, "_brand", "logo.png")),
838
// images/nested-logo.png (referenced as images/nested-logo.png in subdir/brand.yml)
839
// should be copied to _brand/images/nested-logo.png
840
folderExists(join(brandExtSubdirDir, "_brand", "images")),
841
fileExists(join(brandExtSubdirDir, "_brand", "images", "nested-logo.png")),
842
// Verify the content is correct
843
{
844
name: "_brand.yml should contain subdir brand content",
845
verify: () => {
846
const content = Deno.readTextFileSync(join(brandExtSubdirDir, "_brand", "_brand.yml"));
847
if (!content.includes("Test Brand Extension Subdir")) {
848
throw new Error("_brand.yml should contain content from subdir/brand.yml");
849
}
850
return Promise.resolve();
851
}
852
},
853
],
854
{
855
setup: () => {
856
Deno.writeTextFileSync(join(brandExtSubdirDir, "_quarto.yml"), "project:\n type: default\n");
857
return Promise.resolve();
858
},
859
cwd: () => brandExtSubdirDir,
860
teardown: () => {
861
try { Deno.removeSync(brandExtSubdirDir, { recursive: true }); } catch { /* ignore */ }
862
return Promise.resolve();
863
}
864
},
865
"quarto use brand - brand extension with subdir brand file"
866
);
867
868