create-contractor-site 2.1.1 → 2.2.0
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 +144 -0
- package/bin/create-contractor-site.mjs +30 -2
- package/package.json +1 -1
- package/scripts/smoke-test.mjs +713 -5
- package/src/copy-template.mjs +1 -1
- package/src/prompts.mjs +360 -20
- package/src/replace-data.mjs +43 -4
package/scripts/smoke-test.mjs
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* 1. Missing target argument
|
|
7
7
|
* 2. Existing non-empty target
|
|
8
8
|
* 3. Target equal to / inside template root
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
9
|
+
* 4. Published DEFAULT_TEMPLATE_REF / CREATE_CONTRACTOR_TEMPLATE_REF contract (v2.2.0)
|
|
10
|
+
* 5. Duplicate service input normalization + slug uniqueness + business/services alignment
|
|
11
|
+
* 6. Service-area name parse/dedupe (Chesapeake duplicate-slug case) + areas rebuild
|
|
11
12
|
* without leaking stale template county/ZIP metadata into new areas
|
|
12
|
-
*
|
|
13
|
+
* 7. CREATE_CONTRACTOR_SITE_ANSWERS_JSON real CLI spawn (copy+replace, skip setup)
|
|
14
|
+
* 8. Temp-target --yes scaffold (install/validate/build) unless SKIP_CLI_E2E=1
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { spawnSync } from 'node:child_process';
|
|
@@ -26,9 +28,30 @@ import {
|
|
|
26
28
|
replaceTargetData,
|
|
27
29
|
slugify,
|
|
28
30
|
} from '../src/replace-data.mjs';
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
buildAnswers,
|
|
33
|
+
DEFAULT_FREE_ESTIMATE,
|
|
34
|
+
DEFAULT_HOURS,
|
|
35
|
+
DEFAULT_INSURANCE,
|
|
36
|
+
DEFAULT_LICENSE,
|
|
37
|
+
DEFAULT_PAYMENT_METHODS,
|
|
38
|
+
DEFAULT_SITE_TYPE,
|
|
39
|
+
DEFAULT_YEARS_EXPERIENCE,
|
|
40
|
+
normalizeDirectories,
|
|
41
|
+
normalizeHours,
|
|
42
|
+
normalizePaymentMethods,
|
|
43
|
+
normalizeSiteType,
|
|
44
|
+
normalizeSocial,
|
|
45
|
+
optionalText,
|
|
46
|
+
requiredText,
|
|
47
|
+
SITE_TYPES,
|
|
48
|
+
SOCIAL_NETWORK_KEYS,
|
|
49
|
+
} from '../src/prompts.mjs';
|
|
30
50
|
import { isSameOrInside, validateTarget, TargetValidationError } from '../src/validate-target.mjs';
|
|
31
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
DEFAULT_TEMPLATE_REF,
|
|
53
|
+
findLocalTemplateRoot,
|
|
54
|
+
} from '../src/copy-template.mjs';
|
|
32
55
|
import { isVersionAtLeast, resolveCommandPath } from '../src/run-command.mjs';
|
|
33
56
|
|
|
34
57
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +86,32 @@ function assert(condition, message) {
|
|
|
63
86
|
if (!condition) throw new Error(message);
|
|
64
87
|
}
|
|
65
88
|
|
|
89
|
+
/** @param {string} file */
|
|
90
|
+
function readJson(file) {
|
|
91
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Seed template data files into a temp target. @param {string} prefix */
|
|
95
|
+
function seedDataDir(prefix) {
|
|
96
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
97
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
98
|
+
const dataSrc = path.join(REPO_ROOT, 'src', 'data');
|
|
99
|
+
const dataDst = path.join(tmp, 'src', 'data');
|
|
100
|
+
fs.mkdirSync(dataDst, { recursive: true });
|
|
101
|
+
for (const f of [
|
|
102
|
+
'business.json',
|
|
103
|
+
'site.json',
|
|
104
|
+
'services.json',
|
|
105
|
+
'areas.json',
|
|
106
|
+
'navigation.json',
|
|
107
|
+
'landings.json',
|
|
108
|
+
'directories.json',
|
|
109
|
+
]) {
|
|
110
|
+
fs.copyFileSync(path.join(dataSrc, f), path.join(dataDst, f));
|
|
111
|
+
}
|
|
112
|
+
return { tmp, dataDst };
|
|
113
|
+
}
|
|
114
|
+
|
|
66
115
|
/**
|
|
67
116
|
* @param {string[]} args
|
|
68
117
|
* @param {{ env?: NodeJS.ProcessEnv, cwd?: string }} [opts]
|
|
@@ -95,6 +144,43 @@ async function main() {
|
|
|
95
144
|
assert(!isVersionAtLeast('10.9.9', '11.1.2'), 'older major should fail');
|
|
96
145
|
});
|
|
97
146
|
|
|
147
|
+
await test(
|
|
148
|
+
'DEFAULT_TEMPLATE_REF / CREATE_CONTRACTOR_TEMPLATE_REF published contract is v2.2.0',
|
|
149
|
+
() => {
|
|
150
|
+
const copySrcPath = path.join(PKG_ROOT, 'src', 'copy-template.mjs');
|
|
151
|
+
const copySrc = fs.readFileSync(copySrcPath, 'utf8');
|
|
152
|
+
// Pin the source fallback so a silent ref bump cannot ship without updating this test.
|
|
153
|
+
assert(
|
|
154
|
+
/CREATE_CONTRACTOR_TEMPLATE_REF\s*\|\|\s*['"]v2\.2\.0['"]/.test(copySrc),
|
|
155
|
+
'copy-template.mjs DEFAULT_TEMPLATE_REF fallback must be v2.2.0',
|
|
156
|
+
);
|
|
157
|
+
const expected =
|
|
158
|
+
process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.2.0';
|
|
159
|
+
assert(
|
|
160
|
+
DEFAULT_TEMPLATE_REF === expected,
|
|
161
|
+
`DEFAULT_TEMPLATE_REF must equal CREATE_CONTRACTOR_TEMPLATE_REF or v2.2.0 (got ${DEFAULT_TEMPLATE_REF})`,
|
|
162
|
+
);
|
|
163
|
+
if (!process.env.CREATE_CONTRACTOR_TEMPLATE_REF) {
|
|
164
|
+
assert(
|
|
165
|
+
DEFAULT_TEMPLATE_REF === 'v2.2.0',
|
|
166
|
+
`published default template ref must be v2.2.0 (got ${DEFAULT_TEMPLATE_REF})`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const help = runCli(['--help']);
|
|
171
|
+
assert(help.status === 0, `help exit 0, got ${help.status}`);
|
|
172
|
+
const helpOut = `${help.stdout}\n${help.stderr}`;
|
|
173
|
+
assert(
|
|
174
|
+
/CREATE_CONTRACTOR_TEMPLATE_REF/i.test(helpOut),
|
|
175
|
+
'help must document CREATE_CONTRACTOR_TEMPLATE_REF',
|
|
176
|
+
);
|
|
177
|
+
assert(
|
|
178
|
+
/v2\.2\.0/.test(helpOut),
|
|
179
|
+
'help must document default template ref v2.2.0',
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
98
184
|
await test('missing target argument exits 1 with usage', () => {
|
|
99
185
|
const result = runCli([]);
|
|
100
186
|
assert(result.status === 1, `expected exit 1, got ${result.status}`);
|
|
@@ -299,6 +385,7 @@ async function main() {
|
|
|
299
385
|
'areas.json',
|
|
300
386
|
'navigation.json',
|
|
301
387
|
'landings.json',
|
|
388
|
+
'directories.json',
|
|
302
389
|
]) {
|
|
303
390
|
fs.copyFileSync(path.join(dataSrc, file), path.join(dataDst, file));
|
|
304
391
|
}
|
|
@@ -376,6 +463,594 @@ async function main() {
|
|
|
376
463
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
377
464
|
});
|
|
378
465
|
|
|
466
|
+
await test('buildAnswers normalizes blank text trust fields and foundedYear', () => {
|
|
467
|
+
const answers = buildAnswers({
|
|
468
|
+
businessName: 'Trust Co',
|
|
469
|
+
primaryServices: ['Masonry'],
|
|
470
|
+
freeEstimate: ' ',
|
|
471
|
+
yearsExperience: '\t',
|
|
472
|
+
license: '',
|
|
473
|
+
insurance: null,
|
|
474
|
+
foundedYear: ' ',
|
|
475
|
+
});
|
|
476
|
+
assert(answers.freeEstimate === DEFAULT_FREE_ESTIMATE, 'freeEstimate default');
|
|
477
|
+
assert(answers.yearsExperience === DEFAULT_YEARS_EXPERIENCE, 'yearsExperience default');
|
|
478
|
+
assert(answers.license === DEFAULT_LICENSE, 'license default');
|
|
479
|
+
assert(answers.insurance === DEFAULT_INSURANCE, 'insurance default');
|
|
480
|
+
assert(answers.foundedYear === '', 'foundedYear blank → ""');
|
|
481
|
+
assert(requiredText(' x ', 'y') === 'x', 'requiredText trims');
|
|
482
|
+
assert(optionalText(' 2012 ') === '2012', 'optionalText trims');
|
|
483
|
+
assert(optionalText(undefined) === '', 'optionalText missing → ""');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await test('normalizePaymentMethods CSV/array blank → defaults never []', () => {
|
|
487
|
+
assert(
|
|
488
|
+
JSON.stringify(normalizePaymentMethods('Cash, Check , ,Credit Card')) ===
|
|
489
|
+
JSON.stringify(['Cash', 'Check', 'Credit Card']),
|
|
490
|
+
'CSV trims and drops blanks',
|
|
491
|
+
);
|
|
492
|
+
assert(
|
|
493
|
+
JSON.stringify(normalizePaymentMethods([' Visa ', '', 'Cash'])) ===
|
|
494
|
+
JSON.stringify(['Visa', 'Cash']),
|
|
495
|
+
'array trims and drops blanks',
|
|
496
|
+
);
|
|
497
|
+
assert(
|
|
498
|
+
JSON.stringify(normalizePaymentMethods(' ')) ===
|
|
499
|
+
JSON.stringify(DEFAULT_PAYMENT_METHODS),
|
|
500
|
+
'whitespace CSV → defaults',
|
|
501
|
+
);
|
|
502
|
+
assert(
|
|
503
|
+
JSON.stringify(normalizePaymentMethods([])) ===
|
|
504
|
+
JSON.stringify(DEFAULT_PAYMENT_METHODS),
|
|
505
|
+
'empty array → defaults',
|
|
506
|
+
);
|
|
507
|
+
assert(
|
|
508
|
+
JSON.stringify(normalizePaymentMethods(null)) ===
|
|
509
|
+
JSON.stringify(DEFAULT_PAYMENT_METHODS),
|
|
510
|
+
'null → defaults',
|
|
511
|
+
);
|
|
512
|
+
assert(normalizePaymentMethods('').length > 0, 'never empty list');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await test('normalizeHours keeps 3-row {days,time} shape with safe defaults', () => {
|
|
516
|
+
const fromBlank = normalizeHours(undefined);
|
|
517
|
+
assert(fromBlank.length === 3, 'default length 3');
|
|
518
|
+
assert(
|
|
519
|
+
JSON.stringify(fromBlank) === JSON.stringify(DEFAULT_HOURS),
|
|
520
|
+
'missing → DEFAULT_HOURS',
|
|
521
|
+
);
|
|
522
|
+
assert(
|
|
523
|
+
JSON.stringify(normalizeHours([])) === JSON.stringify(DEFAULT_HOURS),
|
|
524
|
+
'empty array → defaults',
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const partial = normalizeHours([
|
|
528
|
+
{ days: ' ', time: '9:00 AM - 5:00 PM' },
|
|
529
|
+
' 10:00 AM - 1:00 PM ',
|
|
530
|
+
{ days: 'Sunday', time: ' ' },
|
|
531
|
+
]);
|
|
532
|
+
assert(partial.length === 3, 'partial still length 3');
|
|
533
|
+
assert(partial[0].days === DEFAULT_HOURS[0].days, 'blank days → default days');
|
|
534
|
+
assert(partial[0].time === '9:00 AM - 5:00 PM', 'weekday time kept');
|
|
535
|
+
assert(partial[1].days === DEFAULT_HOURS[1].days, 'string row uses default days');
|
|
536
|
+
assert(partial[1].time === '10:00 AM - 1:00 PM', 'string row trims time');
|
|
537
|
+
assert(partial[2].days === 'Sunday', 'sunday days kept');
|
|
538
|
+
assert(partial[2].time === DEFAULT_HOURS[2].time, 'blank time → default');
|
|
539
|
+
|
|
540
|
+
const compact = buildAnswers({
|
|
541
|
+
businessName: 'Hours Co',
|
|
542
|
+
primaryServices: ['Masonry'],
|
|
543
|
+
hoursWeekday: '8-5',
|
|
544
|
+
hoursSaturday: ' ',
|
|
545
|
+
hoursSunday: 'Closed',
|
|
546
|
+
});
|
|
547
|
+
assert(compact.hours.length === 3, 'compact → 3 rows');
|
|
548
|
+
assert(compact.hours[0].time === '8-5', 'compact weekday');
|
|
549
|
+
assert(compact.hours[1].time === DEFAULT_HOURS[1].time, 'blank sat → default');
|
|
550
|
+
assert(compact.hours[2].time === 'Closed', 'compact sunday');
|
|
551
|
+
assert(
|
|
552
|
+
JSON.stringify(compact.paymentMethods) === JSON.stringify(DEFAULT_PAYMENT_METHODS),
|
|
553
|
+
'omitted payments → defaults',
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
await test('replaceTargetData writes trust + payment + hours + empty social', () => {
|
|
558
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
559
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-trust-'));
|
|
560
|
+
const dataSrc = path.join(REPO_ROOT, 'src', 'data');
|
|
561
|
+
const dataDst = path.join(tmp, 'src', 'data');
|
|
562
|
+
fs.mkdirSync(dataDst, { recursive: true });
|
|
563
|
+
for (const file of [
|
|
564
|
+
'business.json',
|
|
565
|
+
'site.json',
|
|
566
|
+
'services.json',
|
|
567
|
+
'areas.json',
|
|
568
|
+
'navigation.json',
|
|
569
|
+
'landings.json',
|
|
570
|
+
'directories.json',
|
|
571
|
+
]) {
|
|
572
|
+
fs.copyFileSync(path.join(dataSrc, file), path.join(dataDst, file));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const original = JSON.parse(
|
|
576
|
+
fs.readFileSync(path.join(dataDst, 'business.json'), 'utf8'),
|
|
577
|
+
);
|
|
578
|
+
const originalDirs = JSON.parse(
|
|
579
|
+
fs.readFileSync(path.join(dataDst, 'directories.json'), 'utf8'),
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const answers = buildAnswers({
|
|
583
|
+
businessName: 'Trust Builders',
|
|
584
|
+
primaryServices: ['Masonry'],
|
|
585
|
+
freeEstimate: 'Free Quote Today',
|
|
586
|
+
yearsExperience: '20+',
|
|
587
|
+
license: 'VA Class A #12345',
|
|
588
|
+
insurance: 'Bonded and insured.',
|
|
589
|
+
foundedYear: '',
|
|
590
|
+
paymentMethods: 'Cash, Zelle',
|
|
591
|
+
hours: [
|
|
592
|
+
{ days: 'Monday - Friday', time: '8:00 AM - 4:00 PM' },
|
|
593
|
+
{ days: 'Saturday', time: 'Closed' },
|
|
594
|
+
{ days: 'Sunday', time: 'Closed' },
|
|
595
|
+
],
|
|
596
|
+
});
|
|
597
|
+
replaceTargetData(tmp, answers);
|
|
598
|
+
|
|
599
|
+
const business = JSON.parse(
|
|
600
|
+
fs.readFileSync(path.join(dataDst, 'business.json'), 'utf8'),
|
|
601
|
+
);
|
|
602
|
+
const site = JSON.parse(fs.readFileSync(path.join(dataDst, 'site.json'), 'utf8'));
|
|
603
|
+
const directories = JSON.parse(
|
|
604
|
+
fs.readFileSync(path.join(dataDst, 'directories.json'), 'utf8'),
|
|
605
|
+
);
|
|
606
|
+
assert(business._instructions, '_instructions preserved');
|
|
607
|
+
assert(
|
|
608
|
+
JSON.stringify(Object.keys(business._instructions)) ===
|
|
609
|
+
JSON.stringify(Object.keys(original._instructions)),
|
|
610
|
+
'_instructions keys stable',
|
|
611
|
+
);
|
|
612
|
+
assert(business.free_estimate === 'Free Quote Today', 'free_estimate written');
|
|
613
|
+
assert(business.years_experience === '20+', 'years_experience written');
|
|
614
|
+
assert(business.license === 'VA Class A #12345', 'license written');
|
|
615
|
+
assert(business.insurance === 'Bonded and insured.', 'insurance written');
|
|
616
|
+
assert(
|
|
617
|
+
Object.prototype.hasOwnProperty.call(business, 'founded_year'),
|
|
618
|
+
'founded_year key must remain',
|
|
619
|
+
);
|
|
620
|
+
assert(business.founded_year === '', 'founded_year skip → ""');
|
|
621
|
+
assert(
|
|
622
|
+
JSON.stringify(business.payment_methods) === JSON.stringify(['Cash', 'Zelle']),
|
|
623
|
+
'payment_methods written',
|
|
624
|
+
);
|
|
625
|
+
assert(Array.isArray(business.hours) && business.hours.length === 3, 'hours length 3');
|
|
626
|
+
assert(business.hours[0].days === 'Monday - Friday', 'hours days shape');
|
|
627
|
+
assert(business.hours[0].time === '8:00 AM - 4:00 PM', 'hours time written');
|
|
628
|
+
assert(business.hours[1].time === 'Closed', 'saturday closed');
|
|
629
|
+
assert(JSON.stringify(business.social) === '{}', 'omitted social → {}');
|
|
630
|
+
assert(site.features.enable_directories === false, 'no dirs → enable false');
|
|
631
|
+
assert(directories.directories.length >= 1, 'directories min(1) when none');
|
|
632
|
+
assert(directories.directories.length === originalDirs.directories.length);
|
|
633
|
+
assert(directories._instructions && directories.variant === originalDirs.variant);
|
|
634
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
await test('normalizeSocial/directories + replace populate/none parity', () => {
|
|
638
|
+
// Social blank omission + population (object + compact CSV)
|
|
639
|
+
assert(JSON.stringify(normalizeSocial(undefined)) === '{}', 'missing social → {}');
|
|
640
|
+
assert(
|
|
641
|
+
JSON.stringify(normalizeSocial({ facebook: ' ', instagram: null, x: '' })) === '{}',
|
|
642
|
+
'blank social keys omitted',
|
|
643
|
+
);
|
|
644
|
+
const populated = normalizeSocial({
|
|
645
|
+
facebook: ' https://facebook.com/acme ',
|
|
646
|
+
instagram: '',
|
|
647
|
+
youtube: 'https://youtube.com/@acme',
|
|
648
|
+
unknown: 'https://nope.example',
|
|
649
|
+
});
|
|
650
|
+
assert(
|
|
651
|
+
JSON.stringify(populated) ===
|
|
652
|
+
JSON.stringify({
|
|
653
|
+
facebook: 'https://facebook.com/acme',
|
|
654
|
+
youtube: 'https://youtube.com/@acme',
|
|
655
|
+
}),
|
|
656
|
+
);
|
|
657
|
+
const compact = normalizeSocial(
|
|
658
|
+
'facebook=https://facebook.com/a, google-business=https://g.co/a, ,tiktok=',
|
|
659
|
+
);
|
|
660
|
+
assert(compact.facebook === 'https://facebook.com/a' && compact.google_business === 'https://g.co/a');
|
|
661
|
+
assert(!Object.prototype.hasOwnProperty.call(compact, 'tiktok'));
|
|
662
|
+
assert(SOCIAL_NETWORK_KEYS.includes('linkedin'));
|
|
663
|
+
|
|
664
|
+
// Directories provided vs none
|
|
665
|
+
assert(normalizeDirectories(undefined).length === 0);
|
|
666
|
+
assert(normalizeDirectories([]).length === 0);
|
|
667
|
+
const fromCsv = normalizeDirectories(
|
|
668
|
+
'Google Business|https://g.co/acme, BBB|https://bbb.org/acme ,badrow',
|
|
669
|
+
);
|
|
670
|
+
assert(fromCsv.length === 2 && fromCsv[0].url === 'https://g.co/acme' && fromCsv[0].initials);
|
|
671
|
+
const fromRows = normalizeDirectories([
|
|
672
|
+
{ name: 'Angi', url: 'https://angi.com/x', initials: 'A' },
|
|
673
|
+
{ name: ' ', url: 'https://skip.example' },
|
|
674
|
+
{ name: 'Houzz', url: ' ' },
|
|
675
|
+
]);
|
|
676
|
+
assert(fromRows.length === 1 && fromRows[0].initials === 'A');
|
|
677
|
+
|
|
678
|
+
const none = buildAnswers({ businessName: 'No Social', primaryServices: ['Masonry'] });
|
|
679
|
+
assert(JSON.stringify(none.social) === '{}' && none.directories.length === 0);
|
|
680
|
+
assert(none.enableDirectories === false);
|
|
681
|
+
const withDirs = buildAnswers({
|
|
682
|
+
businessName: 'With Dirs',
|
|
683
|
+
primaryServices: ['Masonry'],
|
|
684
|
+
social: { facebook: 'https://facebook.com/x', instagram: ' ' },
|
|
685
|
+
directories: 'Google|https://g.co/x',
|
|
686
|
+
});
|
|
687
|
+
assert(withDirs.social.facebook === 'https://facebook.com/x');
|
|
688
|
+
assert(!Object.prototype.hasOwnProperty.call(withDirs.social, 'instagram'));
|
|
689
|
+
assert(withDirs.directories.length === 1 && withDirs.enableDirectories === true);
|
|
690
|
+
|
|
691
|
+
// directories:[] + enableDirectories:true → feature false
|
|
692
|
+
const forcedEmpty = buildAnswers({
|
|
693
|
+
businessName: 'Forced Empty Dirs',
|
|
694
|
+
primaryServices: ['Masonry'],
|
|
695
|
+
directories: [],
|
|
696
|
+
enableDirectories: true,
|
|
697
|
+
});
|
|
698
|
+
assert(forcedEmpty.directories.length === 0 && forcedEmpty.enableDirectories === false);
|
|
699
|
+
|
|
700
|
+
// replace: populated social + directories
|
|
701
|
+
const seeded = seedDataDir('ccs-social-');
|
|
702
|
+
replaceTargetData(
|
|
703
|
+
seeded.tmp,
|
|
704
|
+
buildAnswers({
|
|
705
|
+
businessName: 'Social Co',
|
|
706
|
+
primaryServices: ['Patios'],
|
|
707
|
+
social: {
|
|
708
|
+
facebook: 'https://facebook.com/socialco',
|
|
709
|
+
instagram: '',
|
|
710
|
+
youtube: 'https://youtube.com/@socialco',
|
|
711
|
+
},
|
|
712
|
+
directories: [
|
|
713
|
+
{ name: 'Google Business', url: 'https://g.co/socialco', initials: 'G' },
|
|
714
|
+
{ name: 'Angi', url: 'https://angi.com/socialco' },
|
|
715
|
+
],
|
|
716
|
+
}),
|
|
717
|
+
);
|
|
718
|
+
const business = readJson(path.join(seeded.dataDst, 'business.json'));
|
|
719
|
+
const site = readJson(path.join(seeded.dataDst, 'site.json'));
|
|
720
|
+
const directories = readJson(path.join(seeded.dataDst, 'directories.json'));
|
|
721
|
+
assert(
|
|
722
|
+
JSON.stringify(business.social) ===
|
|
723
|
+
JSON.stringify({
|
|
724
|
+
facebook: 'https://facebook.com/socialco',
|
|
725
|
+
youtube: 'https://youtube.com/@socialco',
|
|
726
|
+
}),
|
|
727
|
+
);
|
|
728
|
+
assert(site.features.enable_directories === true);
|
|
729
|
+
assert(directories.directories.length === 2 && directories.directories[0].initials === 'G');
|
|
730
|
+
assert(directories.directories[1].initials && directories._instructions);
|
|
731
|
+
|
|
732
|
+
// replace: directories:[] + enable true → flag false + min(1) placeholder rows
|
|
733
|
+
const forced = seedDataDir('ccs-dirs-force-');
|
|
734
|
+
const origLen = readJson(path.join(forced.dataDst, 'directories.json')).directories.length;
|
|
735
|
+
replaceTargetData(
|
|
736
|
+
forced.tmp,
|
|
737
|
+
buildAnswers({
|
|
738
|
+
businessName: 'Force Enable Empty',
|
|
739
|
+
primaryServices: ['Masonry'],
|
|
740
|
+
directories: [],
|
|
741
|
+
enableDirectories: true,
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
const siteForced = readJson(path.join(forced.dataDst, 'site.json'));
|
|
745
|
+
const dirsForced = readJson(path.join(forced.dataDst, 'directories.json'));
|
|
746
|
+
assert(siteForced.features.enable_directories === false, 'empty+force → enable false');
|
|
747
|
+
assert(dirsForced.directories.length >= 1 && dirsForced.directories.length === origLen);
|
|
748
|
+
fs.rmSync(forced.tmp, { recursive: true, force: true });
|
|
749
|
+
fs.rmSync(seeded.tmp, { recursive: true, force: true });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
await test('normalizeSiteType canonical + human-ish aliases; invalid → multipage', () => {
|
|
753
|
+
assert(DEFAULT_SITE_TYPE === 'multipage', 'default site type is multipage');
|
|
754
|
+
assert(JSON.stringify(SITE_TYPES) === JSON.stringify(['one-page', 'multipage', 'seo']));
|
|
755
|
+
assert(normalizeSiteType(undefined) === 'multipage', 'missing → multipage');
|
|
756
|
+
assert(normalizeSiteType('') === 'multipage', 'blank → multipage');
|
|
757
|
+
assert(normalizeSiteType(' ') === 'multipage', 'whitespace → multipage');
|
|
758
|
+
assert(normalizeSiteType('one-page') === 'one-page');
|
|
759
|
+
assert(normalizeSiteType('multipage') === 'multipage');
|
|
760
|
+
assert(normalizeSiteType('seo') === 'seo');
|
|
761
|
+
assert(normalizeSiteType('one page') === 'one-page', 'one page alias');
|
|
762
|
+
assert(normalizeSiteType('onepage') === 'one-page', 'onepage alias');
|
|
763
|
+
assert(normalizeSiteType('multi page') === 'multipage', 'multi page alias');
|
|
764
|
+
assert(normalizeSiteType('multi-page') === 'multipage', 'multi-page alias');
|
|
765
|
+
assert(normalizeSiteType('SEO') === 'seo', 'case fold');
|
|
766
|
+
assert(normalizeSiteType('not-a-type') === 'multipage', 'invalid → multipage');
|
|
767
|
+
assert(normalizeSiteType('blog') === 'multipage', 'unknown → multipage');
|
|
768
|
+
|
|
769
|
+
const omitted = buildAnswers({ businessName: 'Type Co', primaryServices: ['Masonry'] });
|
|
770
|
+
assert(omitted.siteType === 'multipage', 'buildAnswers omits → multipage');
|
|
771
|
+
assert(buildAnswers({
|
|
772
|
+
businessName: 'One Page Co',
|
|
773
|
+
primaryServices: ['Masonry'],
|
|
774
|
+
siteType: 'one page',
|
|
775
|
+
}).siteType === 'one-page');
|
|
776
|
+
assert(buildAnswers({
|
|
777
|
+
businessName: 'SEO Co',
|
|
778
|
+
primaryServices: ['Masonry'],
|
|
779
|
+
siteType: 'seo',
|
|
780
|
+
}).siteType === 'seo');
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
await test('replaceTargetData writes site_type from answers (default multipage)', () => {
|
|
784
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
785
|
+
const seeded = seedDataDir('ccs-sitetype-');
|
|
786
|
+
try {
|
|
787
|
+
const originalSite = readJson(path.join(seeded.dataDst, 'site.json'));
|
|
788
|
+
replaceTargetData(
|
|
789
|
+
seeded.tmp,
|
|
790
|
+
buildAnswers({ businessName: 'Type Default Co', primaryServices: ['Masonry'] }),
|
|
791
|
+
);
|
|
792
|
+
const siteDefault = readJson(path.join(seeded.dataDst, 'site.json'));
|
|
793
|
+
assert(siteDefault.site_type === 'multipage', `default write multipage, got ${siteDefault.site_type}`);
|
|
794
|
+
assert(siteDefault._instructions, '_instructions preserved');
|
|
795
|
+
assert(
|
|
796
|
+
JSON.stringify(Object.keys(siteDefault._instructions)) ===
|
|
797
|
+
JSON.stringify(Object.keys(originalSite._instructions)),
|
|
798
|
+
'_instructions keys stable',
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// one-page
|
|
802
|
+
const one = seedDataDir('ccs-sitetype-one-');
|
|
803
|
+
replaceTargetData(
|
|
804
|
+
one.tmp,
|
|
805
|
+
buildAnswers({
|
|
806
|
+
businessName: 'One Page Site',
|
|
807
|
+
primaryServices: ['Patios'],
|
|
808
|
+
siteType: 'one-page',
|
|
809
|
+
}),
|
|
810
|
+
);
|
|
811
|
+
assert(readJson(path.join(one.dataDst, 'site.json')).site_type === 'one-page');
|
|
812
|
+
fs.rmSync(one.tmp, { recursive: true, force: true });
|
|
813
|
+
|
|
814
|
+
// seo
|
|
815
|
+
const seo = seedDataDir('ccs-sitetype-seo-');
|
|
816
|
+
replaceTargetData(
|
|
817
|
+
seo.tmp,
|
|
818
|
+
buildAnswers({
|
|
819
|
+
businessName: 'SEO Site',
|
|
820
|
+
primaryServices: ['Patios'],
|
|
821
|
+
siteType: 'seo',
|
|
822
|
+
}),
|
|
823
|
+
);
|
|
824
|
+
assert(readJson(path.join(seo.dataDst, 'site.json')).site_type === 'seo');
|
|
825
|
+
fs.rmSync(seo.tmp, { recursive: true, force: true });
|
|
826
|
+
} finally {
|
|
827
|
+
fs.rmSync(seeded.tmp, { recursive: true, force: true });
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
await test('--help documents trust/payment/hours/social/directories/siteType defaults', () => {
|
|
832
|
+
const help = runCli(['--help']);
|
|
833
|
+
assert(help.status === 0, `help exit 0, got ${help.status}`);
|
|
834
|
+
const helpOut = `${help.stdout}\n${help.stderr}`;
|
|
835
|
+
assert(/freeEstimate/i.test(helpOut), 'help lists freeEstimate');
|
|
836
|
+
assert(/foundedYear/i.test(helpOut), 'help lists foundedYear');
|
|
837
|
+
assert(/empty string/i.test(helpOut), 'help documents foundedYear empty string');
|
|
838
|
+
assert(/paymentMethods/i.test(helpOut), 'help lists paymentMethods');
|
|
839
|
+
assert(/hoursWeekday/i.test(helpOut), 'help lists compact hours keys');
|
|
840
|
+
assert(/social/i.test(helpOut), 'help lists social');
|
|
841
|
+
assert(/directories/i.test(helpOut), 'help lists directories');
|
|
842
|
+
assert(/enable_directories/i.test(helpOut), 'help mentions enable_directories');
|
|
843
|
+
assert(/siteType/i.test(helpOut), 'help lists siteType');
|
|
844
|
+
assert(/multipage/i.test(helpOut), 'help documents multipage default');
|
|
845
|
+
assert(/one-page/i.test(helpOut), 'help lists one-page');
|
|
846
|
+
assert(/CREATE_CONTRACTOR_SITE_ANSWERS_JSON/i.test(helpOut), 'help lists JSON env');
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
await test('CREATE_CONTRACTOR_SITE_ANSWERS_JSON CLI path defaults intake and siteType', () => {
|
|
850
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
851
|
+
// Fresh non-existent target (CLI rejects existing dirs that are non-empty).
|
|
852
|
+
const targetDir = path.join(
|
|
853
|
+
fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-jsonenv-parent-')),
|
|
854
|
+
'site',
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
const payload = {
|
|
858
|
+
businessName: 'JSON Env Masonry',
|
|
859
|
+
primaryServices: ['Patios'],
|
|
860
|
+
// Blank/whitespace trust fields → buildAnswers defaults; foundedYear → ""
|
|
861
|
+
freeEstimate: ' ',
|
|
862
|
+
yearsExperience: '\t',
|
|
863
|
+
license: '',
|
|
864
|
+
insurance: null,
|
|
865
|
+
foundedYear: ' ',
|
|
866
|
+
// Blank payments + empty hours → defaults (never [])
|
|
867
|
+
paymentMethods: ' , ',
|
|
868
|
+
hours: [],
|
|
869
|
+
social: { facebook: ' ', instagram: '' }, // blank → {}
|
|
870
|
+
directories: [], // none → enable false + min(1)
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const result = runCli([targetDir], {
|
|
874
|
+
env: {
|
|
875
|
+
CREATE_CONTRACTOR_TEMPLATE_ROOT: REPO_ROOT,
|
|
876
|
+
CREATE_CONTRACTOR_SITE_ANSWERS_JSON: JSON.stringify(payload),
|
|
877
|
+
NODE_ENV: 'test',
|
|
878
|
+
CREATE_CONTRACTOR_SITE_SKIP_SETUP: '1',
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const out = `${result.stdout}\n${result.stderr}`;
|
|
883
|
+
try {
|
|
884
|
+
assert(result.status === 0, `CLI exit 0, got ${result.status}:\n${out.slice(-2000)}`);
|
|
885
|
+
assert(/Data replace complete/i.test(out), 'expected skip-setup success banner');
|
|
886
|
+
|
|
887
|
+
const businessPath = path.join(targetDir, 'src/data/business.json');
|
|
888
|
+
assert(fs.existsSync(businessPath), 'expected generated business.json');
|
|
889
|
+
const business = JSON.parse(fs.readFileSync(businessPath, 'utf8'));
|
|
890
|
+
const site = readJson(path.join(targetDir, 'src/data/site.json'));
|
|
891
|
+
const directories = readJson(path.join(targetDir, 'src/data/directories.json'));
|
|
892
|
+
|
|
893
|
+
assert(business.name === 'JSON Env Masonry', 'env business name via CLI');
|
|
894
|
+
assert(
|
|
895
|
+
business.free_estimate === DEFAULT_FREE_ESTIMATE,
|
|
896
|
+
`free_estimate default via CLI, got ${business.free_estimate}`,
|
|
897
|
+
);
|
|
898
|
+
assert(
|
|
899
|
+
business.years_experience === DEFAULT_YEARS_EXPERIENCE,
|
|
900
|
+
`years_experience default via CLI, got ${business.years_experience}`,
|
|
901
|
+
);
|
|
902
|
+
assert(
|
|
903
|
+
business.license === DEFAULT_LICENSE,
|
|
904
|
+
`license default via CLI, got ${business.license}`,
|
|
905
|
+
);
|
|
906
|
+
assert(
|
|
907
|
+
business.insurance === DEFAULT_INSURANCE,
|
|
908
|
+
`insurance default via CLI, got ${business.insurance}`,
|
|
909
|
+
);
|
|
910
|
+
assert(
|
|
911
|
+
Object.prototype.hasOwnProperty.call(business, 'founded_year'),
|
|
912
|
+
'founded_year key must remain',
|
|
913
|
+
);
|
|
914
|
+
assert(business.founded_year === '', 'CLI founded_year blank → ""');
|
|
915
|
+
assert(
|
|
916
|
+
JSON.stringify(business.payment_methods) === JSON.stringify(DEFAULT_PAYMENT_METHODS),
|
|
917
|
+
'CLI blank payments → defaults',
|
|
918
|
+
);
|
|
919
|
+
assert(business.payment_methods.length > 0, 'payment_methods never []');
|
|
920
|
+
assert(
|
|
921
|
+
JSON.stringify(business.hours) === JSON.stringify(DEFAULT_HOURS),
|
|
922
|
+
'CLI empty hours → DEFAULT_HOURS',
|
|
923
|
+
);
|
|
924
|
+
assert(business.hours.every((h) => h.days && h.time), 'hours rows shaped');
|
|
925
|
+
assert(JSON.stringify(business.social) === '{}', 'CLI blank social → {}');
|
|
926
|
+
assert(site.features.enable_directories === false, 'CLI no dirs → enable false');
|
|
927
|
+
assert(directories.directories.length >= 1, 'CLI directories min(1)');
|
|
928
|
+
assert(site.site_type === 'multipage', `CLI omitted siteType → multipage, got ${site.site_type}`);
|
|
929
|
+
} finally {
|
|
930
|
+
const parent = path.dirname(targetDir);
|
|
931
|
+
try {
|
|
932
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
933
|
+
} catch {
|
|
934
|
+
console.warn(` (could not fully remove ${parent})`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
await test('CREATE_CONTRACTOR_SITE_ANSWERS_JSON can set siteType one-page and seo', () => {
|
|
940
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
941
|
+
|
|
942
|
+
for (const { label, siteType, expected } of [
|
|
943
|
+
{ label: 'one-page', siteType: 'one page', expected: 'one-page' },
|
|
944
|
+
{ label: 'seo', siteType: 'seo', expected: 'seo' },
|
|
945
|
+
]) {
|
|
946
|
+
const targetDir = path.join(
|
|
947
|
+
fs.mkdtempSync(path.join(os.tmpdir(), `ccs-sitetype-${label}-`)),
|
|
948
|
+
'site',
|
|
949
|
+
);
|
|
950
|
+
const result = runCli([targetDir], {
|
|
951
|
+
env: {
|
|
952
|
+
CREATE_CONTRACTOR_TEMPLATE_ROOT: REPO_ROOT,
|
|
953
|
+
CREATE_CONTRACTOR_SITE_ANSWERS_JSON: JSON.stringify({
|
|
954
|
+
businessName: `JSON ${label}`,
|
|
955
|
+
primaryServices: ['Patios'],
|
|
956
|
+
siteType,
|
|
957
|
+
}),
|
|
958
|
+
NODE_ENV: 'test',
|
|
959
|
+
CREATE_CONTRACTOR_SITE_SKIP_SETUP: '1',
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
const out = `${result.stdout}\n${result.stderr}`;
|
|
963
|
+
try {
|
|
964
|
+
assert(result.status === 0, `CLI ${label} exit 0, got ${result.status}:\n${out.slice(-1500)}`);
|
|
965
|
+
const site = readJson(path.join(targetDir, 'src/data/site.json'));
|
|
966
|
+
assert(site.site_type === expected, `CLI ${label} site_type, got ${site.site_type}`);
|
|
967
|
+
} finally {
|
|
968
|
+
try {
|
|
969
|
+
fs.rmSync(path.dirname(targetDir), { recursive: true, force: true });
|
|
970
|
+
} catch {
|
|
971
|
+
console.warn(` (could not fully remove ${path.dirname(targetDir)})`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
await test('CREATE_CONTRACTOR_SITE_ANSWERS_JSON compact payment/hours/social/dirs path', () => {
|
|
978
|
+
assert(REPO_ROOT, 'expected local template root');
|
|
979
|
+
const targetDir = path.join(
|
|
980
|
+
fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-jsoncompact-parent-')),
|
|
981
|
+
'site',
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
const payload = {
|
|
985
|
+
businessName: 'Compact Hours Co',
|
|
986
|
+
primaryServices: ['Patios'],
|
|
987
|
+
paymentMethods: ['Cash', ' ', 'ACH'],
|
|
988
|
+
hoursWeekday: '9:00 AM - 5:00 PM',
|
|
989
|
+
hoursSaturday: 'Closed',
|
|
990
|
+
hoursSunday: 'Closed',
|
|
991
|
+
social: 'facebook=https://facebook.com/compact,instagram=https://instagram.com/compact',
|
|
992
|
+
directories: 'Google Business|https://g.co/compact,BBB|https://bbb.org/compact',
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const result = runCli([targetDir], {
|
|
996
|
+
env: {
|
|
997
|
+
CREATE_CONTRACTOR_TEMPLATE_ROOT: REPO_ROOT,
|
|
998
|
+
CREATE_CONTRACTOR_SITE_ANSWERS_JSON: JSON.stringify(payload),
|
|
999
|
+
NODE_ENV: 'test',
|
|
1000
|
+
CREATE_CONTRACTOR_SITE_SKIP_SETUP: '1',
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
const out = `${result.stdout}\n${result.stderr}`;
|
|
1005
|
+
try {
|
|
1006
|
+
assert(result.status === 0, `CLI exit 0, got ${result.status}:\n${out.slice(-2000)}`);
|
|
1007
|
+
const business = JSON.parse(
|
|
1008
|
+
fs.readFileSync(path.join(targetDir, 'src/data/business.json'), 'utf8'),
|
|
1009
|
+
);
|
|
1010
|
+
const site = readJson(path.join(targetDir, 'src/data/site.json'));
|
|
1011
|
+
const directories = readJson(path.join(targetDir, 'src/data/directories.json'));
|
|
1012
|
+
assert(
|
|
1013
|
+
JSON.stringify(business.payment_methods) === JSON.stringify(['Cash', 'ACH']),
|
|
1014
|
+
`compact payments, got ${JSON.stringify(business.payment_methods)}`,
|
|
1015
|
+
);
|
|
1016
|
+
assert(business.hours.length === 3, 'compact hours length');
|
|
1017
|
+
assert(business.hours[0].days === 'Monday - Friday', 'weekday days label');
|
|
1018
|
+
assert(business.hours[0].time === '9:00 AM - 5:00 PM', 'compact weekday time');
|
|
1019
|
+
assert(business.hours[1].time === 'Closed', 'compact saturday');
|
|
1020
|
+
assert(business.hours[2].time === 'Closed', 'compact sunday');
|
|
1021
|
+
// Trust defaults still apply when omitted
|
|
1022
|
+
assert(business.free_estimate === DEFAULT_FREE_ESTIMATE, 'trust default still works');
|
|
1023
|
+
assert(business.founded_year === '', 'founded_year still ""');
|
|
1024
|
+
assert(business.social.facebook === 'https://facebook.com/compact');
|
|
1025
|
+
assert(business.social.instagram === 'https://instagram.com/compact');
|
|
1026
|
+
assert(site.features.enable_directories === true, 'compact dirs → enable true');
|
|
1027
|
+
assert(directories.directories.length === 2);
|
|
1028
|
+
assert(directories.directories[0].url === 'https://g.co/compact');
|
|
1029
|
+
} finally {
|
|
1030
|
+
const parent = path.dirname(targetDir);
|
|
1031
|
+
try {
|
|
1032
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
1033
|
+
} catch {
|
|
1034
|
+
console.warn(` (could not fully remove ${parent})`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
await test('skip setup guard requires NODE_ENV test', () => {
|
|
1040
|
+
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-skip-guard-'));
|
|
1041
|
+
const targetDir = path.join(parent, 'site');
|
|
1042
|
+
try {
|
|
1043
|
+
const result = runCli(['--yes', targetDir], {
|
|
1044
|
+
env: { CREATE_CONTRACTOR_TEMPLATE_ROOT: REPO_ROOT || '', CREATE_CONTRACTOR_SITE_SKIP_SETUP: '1', NODE_ENV: '' },
|
|
1045
|
+
});
|
|
1046
|
+
const out = `${result.stdout}\n${result.stderr}`;
|
|
1047
|
+
assert(result.status === 1, `expected guard failure, got ${result.status}`);
|
|
1048
|
+
assert(/requires NODE_ENV=test/i.test(out), `expected guard message:\n${out}`);
|
|
1049
|
+
} finally {
|
|
1050
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
379
1054
|
await test('replaceTargetData Chesapeake case keeps unique area slugs', () => {
|
|
380
1055
|
assert(REPO_ROOT, 'expected local template root');
|
|
381
1056
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-areas-'));
|
|
@@ -389,6 +1064,7 @@ async function main() {
|
|
|
389
1064
|
'areas.json',
|
|
390
1065
|
'navigation.json',
|
|
391
1066
|
'landings.json',
|
|
1067
|
+
'directories.json',
|
|
392
1068
|
]) {
|
|
393
1069
|
fs.copyFileSync(path.join(dataSrc, file), path.join(dataDst, file));
|
|
394
1070
|
}
|
|
@@ -467,6 +1143,38 @@ async function main() {
|
|
|
467
1143
|
fs.readFileSync(path.join(targetDir, 'src/data/business.json'), 'utf8'),
|
|
468
1144
|
);
|
|
469
1145
|
assert(business.name === 'Acme Masonry', 'expected replaced business name');
|
|
1146
|
+
// --yes omits trust/payment/hours → buildAnswers defaults (not sample-specific facts)
|
|
1147
|
+
assert(business.free_estimate === DEFAULT_FREE_ESTIMATE, 'E2E free_estimate default');
|
|
1148
|
+
assert(
|
|
1149
|
+
business.years_experience === DEFAULT_YEARS_EXPERIENCE,
|
|
1150
|
+
'E2E years_experience default',
|
|
1151
|
+
);
|
|
1152
|
+
assert(business.license === DEFAULT_LICENSE, 'E2E license default');
|
|
1153
|
+
assert(business.insurance === DEFAULT_INSURANCE, 'E2E insurance default');
|
|
1154
|
+
assert(business.founded_year === '', 'E2E founded_year omitted → ""');
|
|
1155
|
+
assert(
|
|
1156
|
+
JSON.stringify(business.payment_methods) === JSON.stringify(DEFAULT_PAYMENT_METHODS),
|
|
1157
|
+
'E2E payment_methods defaults',
|
|
1158
|
+
);
|
|
1159
|
+
assert(business.payment_methods.length > 0, 'E2E payment_methods never []');
|
|
1160
|
+
assert(
|
|
1161
|
+
JSON.stringify(business.hours) === JSON.stringify(DEFAULT_HOURS),
|
|
1162
|
+
'E2E hours defaults',
|
|
1163
|
+
);
|
|
1164
|
+
assert(
|
|
1165
|
+
business.hours.every((h) => typeof h.days === 'string' && typeof h.time === 'string'),
|
|
1166
|
+
'E2E hours shape',
|
|
1167
|
+
);
|
|
1168
|
+
assert(JSON.stringify(business.social) === '{}', 'E2E omitted social → {}');
|
|
1169
|
+
const siteE2E = readJson(path.join(targetDir, 'src/data/site.json'));
|
|
1170
|
+
const dirsE2E = readJson(path.join(targetDir, 'src/data/directories.json'));
|
|
1171
|
+
assert(siteE2E.features.enable_directories === false, 'E2E no dirs → enable false');
|
|
1172
|
+
assert(dirsE2E.directories.length >= 1, 'E2E directories min(1)');
|
|
1173
|
+
assert(
|
|
1174
|
+
siteE2E.site_type === 'multipage',
|
|
1175
|
+
`E2E --yes site_type multipage, got ${siteE2E.site_type}`,
|
|
1176
|
+
);
|
|
1177
|
+
// Full scaffold already ran validate:data + build (includes route gate) successfully.
|
|
470
1178
|
|
|
471
1179
|
const services = JSON.parse(
|
|
472
1180
|
fs.readFileSync(path.join(targetDir, 'src/data/services.json'), 'utf8'),
|