@brandlab-digital/conformance 2.1.0 → 3.0.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.
Files changed (67) hide show
  1. package/dist/analytics-e2e.d.ts +52 -0
  2. package/dist/analytics-e2e.d.ts.map +1 -0
  3. package/dist/analytics-e2e.js +189 -0
  4. package/dist/analytics-e2e.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +40 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/contact-e2e.d.ts +43 -0
  10. package/dist/contact-e2e.d.ts.map +1 -0
  11. package/dist/contact-e2e.js +193 -0
  12. package/dist/contact-e2e.js.map +1 -0
  13. package/dist/current-release.js +7 -7
  14. package/dist/current-release.js.map +1 -1
  15. package/dist/e2e-support.d.ts +42 -0
  16. package/dist/e2e-support.d.ts.map +1 -0
  17. package/dist/e2e-support.js +126 -0
  18. package/dist/e2e-support.js.map +1 -0
  19. package/dist/engineering.d.ts +14 -0
  20. package/dist/engineering.d.ts.map +1 -1
  21. package/dist/engineering.js +446 -50
  22. package/dist/engineering.js.map +1 -1
  23. package/dist/estate.d.ts +105 -0
  24. package/dist/estate.d.ts.map +1 -0
  25. package/dist/estate.js +391 -0
  26. package/dist/estate.js.map +1 -0
  27. package/dist/families-e2e.d.ts +45 -0
  28. package/dist/families-e2e.d.ts.map +1 -0
  29. package/dist/families-e2e.js +248 -0
  30. package/dist/families-e2e.js.map +1 -0
  31. package/dist/glob.d.ts +21 -0
  32. package/dist/glob.d.ts.map +1 -0
  33. package/dist/glob.js +138 -0
  34. package/dist/glob.js.map +1 -0
  35. package/dist/knip.d.ts +47 -0
  36. package/dist/knip.d.ts.map +1 -0
  37. package/dist/knip.js +84 -0
  38. package/dist/knip.js.map +1 -0
  39. package/dist/project.d.ts +4 -1
  40. package/dist/project.d.ts.map +1 -1
  41. package/dist/project.js +163 -147
  42. package/dist/project.js.map +1 -1
  43. package/dist/public-copy.d.ts +36 -0
  44. package/dist/public-copy.d.ts.map +1 -0
  45. package/dist/public-copy.js +517 -0
  46. package/dist/public-copy.js.map +1 -0
  47. package/dist/rule-registry.d.ts +292 -0
  48. package/dist/rule-registry.d.ts.map +1 -0
  49. package/dist/rule-registry.js +68 -0
  50. package/dist/rule-registry.js.map +1 -0
  51. package/dist/search-certification.d.ts +4 -0
  52. package/dist/search-certification.d.ts.map +1 -1
  53. package/dist/search-certification.js +17 -3
  54. package/dist/search-certification.js.map +1 -1
  55. package/dist/source-files.d.ts +54 -0
  56. package/dist/source-files.d.ts.map +1 -0
  57. package/dist/source-files.js +323 -0
  58. package/dist/source-files.js.map +1 -0
  59. package/dist/static-rules.d.ts +45 -0
  60. package/dist/static-rules.d.ts.map +1 -0
  61. package/dist/static-rules.js +762 -0
  62. package/dist/static-rules.js.map +1 -0
  63. package/dist/verification-policy.d.ts +13 -4
  64. package/dist/verification-policy.d.ts.map +1 -1
  65. package/dist/verification-policy.js +31 -12
  66. package/dist/verification-policy.js.map +1 -1
  67. package/package.json +37 -1
package/dist/estate.js ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * The shared technical-estate journey for a Brandlab production build:
3
+ *
4
+ * // tests/e2e/estate.spec.ts
5
+ * import { defineBrandlabEstateTests } from '@brandlab-digital/conformance/estate'
6
+ * import { site } from '../../src/config/site'
7
+ * import { routes } from '../../src/data/routes'
8
+ * import { redirects } from '../../src/data/redirects'
9
+ *
10
+ * defineBrandlabEstateTests({ site, routes, redirects })
11
+ *
12
+ * It asserts the estate and its routes: the sitemap, each route's status,
13
+ * canonical and indexing, internal links, redirects, unknown slugs and
14
+ * robots.txt. Page and structured-data quality is reported, never enforced,
15
+ * by defineBrandlabPageQualityTests: a backend release keeps production's pages
16
+ * as they are, which the comparison harness proves. Counts are always derived
17
+ * from the current catalogue, never a historic constant.
18
+ */
19
+ import { expect, test } from '@playwright/test';
20
+ import { extractSearchCertificationHtmlSnapshot } from './search-certification.js';
21
+ const BUSINESS_TYPES = new Set([
22
+ 'LocalBusiness', 'HomeAndConstructionBusiness', 'GeneralContractor', 'Plumber', 'Electrician',
23
+ 'RoofingContractor', 'HousePainter', 'Locksmith', 'MovingCompany', 'HVACBusiness', 'ProfessionalService',
24
+ ]);
25
+ const UNKNOWN_SLUG = 'brandlab-estate-unknown-page';
26
+ const NOT_FOUND_ROUTE = '/404';
27
+ const PATTERN_SAMPLE = 'brandlab-estate-sample';
28
+ const ASSET_PATH = /\.[a-z0-9]{2,5}$/i;
29
+ export function defineBrandlabEstateTests(options) {
30
+ const origin = new URL(String(options.site.canonicalUrl)).origin;
31
+ const canonical = (path) => new URL(path, `${origin}/`).href;
32
+ const routes = options.routes;
33
+ const knownPaths = new Set(routes.map((route) => normalisePath(route.path)));
34
+ const redirects = (options.redirects ?? []).filter((rule) => !rule.host);
35
+ const redirectBySource = new Map(redirects.filter((rule) => !isRedirectPattern(rule.source)).map((rule) => [normalisePath(rule.source), rule]));
36
+ const patternRedirects = redirects.flatMap((rule) => {
37
+ const matcher = isRedirectPattern(rule.source) ? redirectSourceMatcher(rule.source) : undefined;
38
+ return matcher ? [{ rule, matcher }] : [];
39
+ });
40
+ const findRedirect = (path) => redirectBySource.get(path) ?? patternRedirects.find(({ matcher }) => matcher.test(path))?.rule;
41
+ const timeout = options.timeout ?? 240_000;
42
+ test.describe('Brandlab technical estate', () => {
43
+ test('the sitemap lists exactly the indexable catalogue routes', async ({ request }) => {
44
+ test.setTimeout(timeout);
45
+ const listed = await readSitemap(request, '/sitemap.xml', origin);
46
+ const expected = routes
47
+ .filter((route) => route.indexing === 'index')
48
+ .map((route) => canonical(route.canonicalPath ?? route.path));
49
+ expect(listed.length, 'the sitemap repeats a URL').toBe(new Set(listed).size);
50
+ expect([...listed].sort()).toEqual([...new Set(expected)].sort());
51
+ });
52
+ test('every catalogue route serves its canonical identity and links only to live pages', async ({ request }, testInfo) => {
53
+ test.setTimeout(timeout);
54
+ const failures = [];
55
+ const internalLinks = new Map();
56
+ const checked = [];
57
+ await eachWithConcurrency(routes, options.concurrency ?? 8, async (route) => {
58
+ const [get, head] = await Promise.all([
59
+ request.get(route.path, { maxRedirects: 0 }),
60
+ request.head(route.path, { maxRedirects: 0 }),
61
+ ]);
62
+ // Next serves its not-found page at /404 with a 404 status.
63
+ const notFound = normalisePath(route.path) === NOT_FOUND_ROUTE;
64
+ const expectedStatus = notFound ? 404 : 200;
65
+ if (get.status() !== expectedStatus || head.status() !== expectedStatus) {
66
+ failures.push(`${route.path}: GET ${get.status()}, HEAD ${head.status()} (expected ${expectedStatus})`);
67
+ return;
68
+ }
69
+ const html = await get.text();
70
+ const snapshot = extractSearchCertificationHtmlSnapshot(html);
71
+ const robots = `${snapshot.robots ?? ''} ${get.headers()['x-robots-tag'] ?? ''}`;
72
+ if (notFound) {
73
+ if (!/noindex/i.test(robots))
74
+ failures.push(`${route.path}: the not-found page is indexable`);
75
+ checked.push({ path: route.path, canonical: snapshot.canonical, jsonLd: snapshot.jsonLd.length });
76
+ return;
77
+ }
78
+ const expectedCanonical = canonical(route.canonicalPath ?? route.path);
79
+ const canonicalTags = html.match(/<link\b[^>]*\brel=["']canonical["'][^>]*>/gi) ?? [];
80
+ if (canonicalTags.length !== 1)
81
+ failures.push(`${route.path}: ${canonicalTags.length} canonical links`);
82
+ if (!snapshot.canonical || new URL(snapshot.canonical, origin).href !== expectedCanonical) {
83
+ failures.push(`${route.path}: canonical ${snapshot.canonical ?? 'missing'} (expected ${expectedCanonical})`);
84
+ }
85
+ if (route.indexing === 'index' && /noindex/i.test(robots))
86
+ failures.push(`${route.path}: indexable route is noindex`);
87
+ if (route.indexing !== 'index' && !/noindex/i.test(robots))
88
+ failures.push(`${route.path}: ${route.indexing} route is indexable`);
89
+ for (const href of extractLinks(html)) {
90
+ const path = internalPath(href, expectedCanonical, origin);
91
+ if (path && !internalLinks.has(path))
92
+ internalLinks.set(path, route.path);
93
+ }
94
+ checked.push({ path: route.path, canonical: snapshot.canonical, jsonLd: snapshot.jsonLd.length });
95
+ });
96
+ for (const [path, from] of [...internalLinks].sort()) {
97
+ if (knownPaths.has(path))
98
+ continue;
99
+ const redirect = findRedirect(path);
100
+ if (redirect && isExternalDestination(redirect.destination))
101
+ continue;
102
+ failures.push(redirect
103
+ ? `${from}: links to ${path}, which redirects; link to ${redirect.destination} directly`
104
+ : `${from}: links to ${path}, which is not a catalogue route`);
105
+ }
106
+ await testInfo.attach('technical-estate.json', {
107
+ body: JSON.stringify({ routes: checked.sort((a, b) => a.path.localeCompare(b.path)), internalLinks: [...internalLinks.keys()].sort() }, null, 2),
108
+ contentType: 'application/json',
109
+ });
110
+ expect(failures).toEqual([]);
111
+ });
112
+ test('every redirect resolves in one hop and keeps the query string', async ({ request }, testInfo) => {
113
+ test.setTimeout(timeout);
114
+ const failures = [];
115
+ const skipped = [];
116
+ for (const rule of redirects) {
117
+ const probe = redirectProbe(rule);
118
+ if (!probe) {
119
+ skipped.push(`${rule.source} → ${rule.destination}`);
120
+ continue;
121
+ }
122
+ const sources = [probe.source, ...(options.trailingSlashRedirects === false || probe.source === '/' || probe.source.endsWith('/') ? [] : [`${probe.source}/`])];
123
+ for (const source of sources) {
124
+ for (const method of ['GET', 'HEAD']) {
125
+ const response = await request.fetch(`${source}?brandlab_estate=1`, { method, maxRedirects: 0 });
126
+ const location = response.headers().location;
127
+ const expectedStatus = source === probe.source ? [rule.status] : [301, 308];
128
+ if (!expectedStatus.includes(response.status()) || !location) {
129
+ failures.push(`${method} ${source}: ${response.status()} (expected ${expectedStatus.join(' or ')} to ${probe.destination})`);
130
+ continue;
131
+ }
132
+ const target = new URL(location, origin);
133
+ if (isExternalDestination(probe.destination)) {
134
+ if (!target.href.startsWith(new URL(probe.destination, origin).href.replace(/\/$/, '')))
135
+ failures.push(`${method} ${source}: Location ${location} (expected ${probe.destination})`);
136
+ continue;
137
+ }
138
+ if (target.pathname !== new URL(probe.destination, origin).pathname)
139
+ failures.push(`${method} ${source}: Location ${location} (expected ${probe.destination})`);
140
+ if (target.searchParams.get('brandlab_estate') !== '1')
141
+ failures.push(`${method} ${source}: the query string was dropped`);
142
+ }
143
+ }
144
+ // A destination with parameters names many pages; the sample fills it with a page that does not exist.
145
+ if (!isExternalDestination(probe.destination) && !probe.sampled) {
146
+ const destination = await request.get(probe.destination, { maxRedirects: 0 });
147
+ if (destination.status() !== 200)
148
+ failures.push(`${rule.source}: destination ${probe.destination} answers ${destination.status()} (a chain or a missing page)`);
149
+ }
150
+ }
151
+ if (skipped.length > 0) {
152
+ testInfo.annotations.push({ type: 'skipped-redirects', description: `${skipped.length} redirect(s) with a parameter pattern that cannot be sampled: ${skipped.join('; ')}` });
153
+ }
154
+ expect(failures).toEqual([]);
155
+ });
156
+ test('unknown slugs answer 404 instead of inventing pages', async ({ request }) => {
157
+ const paths = options.notFoundPaths ?? defaultNotFoundPaths(routes);
158
+ const failures = [];
159
+ for (const path of paths) {
160
+ const response = await request.get(path, { maxRedirects: 0 });
161
+ if (response.status() !== 404)
162
+ failures.push(`${path}: ${response.status()} (expected 404)`);
163
+ }
164
+ expect(failures).toEqual([]);
165
+ });
166
+ test('robots.txt allows crawling and names the sitemap', async ({ request }) => {
167
+ const response = await request.get('/robots.txt');
168
+ expect(response.status()).toBe(200);
169
+ const body = await response.text();
170
+ expect(body).toMatch(new RegExp(`^sitemap:\\s*${escapeRegExp(`${origin}/sitemap.xml`)}\\s*$`, 'im'));
171
+ expect(body).not.toMatch(/^disallow:\s*\/\s*$/im);
172
+ });
173
+ });
174
+ }
175
+ /**
176
+ * Every `<loc>` in a sitemap, following one level of sitemap index. Nothing is
177
+ * followed through a redirect, and with `origin` every nested sitemap must be
178
+ * on that origin (it is then fetched from the build under test).
179
+ */
180
+ export async function readSitemap(request, path, origin) {
181
+ const read = async (target) => {
182
+ const response = await request.get(target, { maxRedirects: 0 });
183
+ if (response.status() !== 200)
184
+ throw new Error(`${target} answers ${response.status()}; a sitemap must answer 200 without redirecting.`);
185
+ return response.text();
186
+ };
187
+ const xml = await read(path);
188
+ const locations = extractSitemapLocations(xml);
189
+ if (!/<sitemapindex\b/i.test(xml))
190
+ return locations;
191
+ const nested = [];
192
+ for (const location of locations) {
193
+ const url = new URL(location);
194
+ if (origin && url.origin !== new URL(origin).origin) {
195
+ throw new Error(`The sitemap index lists ${location}, which is not on ${new URL(origin).origin}.`);
196
+ }
197
+ nested.push(...extractSitemapLocations(await read(`${url.pathname}${url.search}`)));
198
+ }
199
+ return nested;
200
+ }
201
+ export function extractSitemapLocations(xml) {
202
+ return [...xml.matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/gi)].map((match) => new URL(decodeXml(match[1])).href);
203
+ }
204
+ export function defaultNotFoundPaths(routes) {
205
+ const parents = new Set(['']);
206
+ for (const route of routes) {
207
+ const segments = normalisePath(route.path).split('/').filter(Boolean);
208
+ if (segments.length > 1)
209
+ parents.add(`/${segments.slice(0, -1).join('/')}`);
210
+ if (segments.length === 1)
211
+ parents.add(`/${segments[0]}`);
212
+ }
213
+ return [...parents].sort().map((parent) => `${parent}/${UNKNOWN_SLUG}`);
214
+ }
215
+ /** A Next redirect path with parameters: `:name`, `:name*`, `:name+`, `:name?` or a regular-expression group. */
216
+ export function isRedirectPattern(path) {
217
+ return /:[A-Za-z_]\w*|\(/.test(path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ''));
218
+ }
219
+ /**
220
+ * The request that proves a redirect rule: a pattern gets one sample segment
221
+ * per parameter, in its source and destination alike. Undefined when a
222
+ * parameter carries its own regular expression, which no sample can satisfy
223
+ * with certainty.
224
+ */
225
+ export function redirectProbe(rule) {
226
+ const sample = (path) => {
227
+ if (!isRedirectPattern(path))
228
+ return path;
229
+ // Keep an external destination's scheme and host; parameters live in the path.
230
+ const origin = /^[a-z][a-z0-9+.-]*:\/\/[^/]*/i.exec(path)?.[0] ?? '';
231
+ const rest = path.slice(origin.length);
232
+ if (rest.includes('('))
233
+ return undefined;
234
+ return `${origin}${rest.replace(/:[A-Za-z_]\w*[*+?]?/g, PATTERN_SAMPLE)}`;
235
+ };
236
+ const source = sample(rule.source);
237
+ const destination = sample(rule.destination);
238
+ if (source === undefined || destination === undefined)
239
+ return undefined;
240
+ return { source, destination, sampled: isRedirectPattern(rule.destination) };
241
+ }
242
+ /** A matcher for paths a pattern source redirects, for internal-link checks. */
243
+ function redirectSourceMatcher(source) {
244
+ if (source.includes('('))
245
+ return undefined;
246
+ const pattern = normalisePath(source).split(/(:[A-Za-z_]\w*[*+?]?)/).map((part) => {
247
+ const parameter = /^:[A-Za-z_]\w*([*+?]?)$/.exec(part);
248
+ if (!parameter)
249
+ return escapeRegExp(part);
250
+ return parameter[1] === '*' || parameter[1] === '+' ? '.+' : parameter[1] === '?' ? '[^/]*' : '[^/]+';
251
+ }).join('');
252
+ return new RegExp(`^${pattern}$`);
253
+ }
254
+ /**
255
+ * The advisory page-quality journey: headings, title, description, og:url,
256
+ * implementation wording and structured-data identity for every catalogue
257
+ * page. It reports what it finds (page-quality.json and an annotation) and
258
+ * never fails, because a backend release keeps production's pages as they
259
+ * are; the comparison harness proves them unchanged.
260
+ *
261
+ * // tests/e2e/page-quality.spec.ts
262
+ * defineBrandlabPageQualityTests({ site, routes })
263
+ */
264
+ export function defineBrandlabPageQualityTests(options) {
265
+ const origin = new URL(String(options.site.canonicalUrl)).origin;
266
+ const canonical = (path) => new URL(path, `${origin}/`).href;
267
+ const businessId = options.businessId ?? `${origin}/#business`;
268
+ const timeout = options.timeout ?? 240_000;
269
+ const routes = options.routes.filter((route) => normalisePath(route.path) !== NOT_FOUND_ROUTE);
270
+ test.describe('Brandlab page quality (advisory)', () => {
271
+ test('reports page and structured-data quality without failing', async ({ request }, testInfo) => {
272
+ test.setTimeout(timeout);
273
+ const findings = [];
274
+ await eachWithConcurrency(routes, options.concurrency ?? 8, async (route) => {
275
+ const response = await request.get(route.path, { maxRedirects: 0 });
276
+ // The estate journey owns status codes; quality is read from the pages that load.
277
+ if (response.status() !== 200)
278
+ return;
279
+ const html = await response.text();
280
+ const snapshot = extractSearchCertificationHtmlSnapshot(html);
281
+ const add = (kind, detail) => findings.push({ path: route.path, kind, detail });
282
+ const headings = html.match(/<h1(?:\s|>)/gi)?.length ?? 0;
283
+ if (headings !== 1)
284
+ add('headings', `${headings} h1 headings`);
285
+ if (!snapshot.title)
286
+ add('title', 'no title');
287
+ if (!snapshot.description)
288
+ add('description', 'no meta description');
289
+ const expectedCanonical = canonical(route.canonicalPath ?? route.path);
290
+ const ogUrl = snapshot.openGraph['og:url'];
291
+ if (ogUrl && new URL(ogUrl, origin).href !== expectedCanonical)
292
+ add('og-url', `og:url ${ogUrl} is not the canonical URL`);
293
+ if (snapshot.implementationCopyMatches.length > 0)
294
+ add('implementation-wording', snapshot.implementationCopyMatches.join(', '));
295
+ for (const failure of inspectStructuredData(route, snapshot.jsonLd, businessId)) {
296
+ add('structured-data', failure.slice(route.path.length + 2));
297
+ }
298
+ });
299
+ findings.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
300
+ const byKind = Object.fromEntries([...new Set(findings.map((finding) => finding.kind))].sort().map((kind) => [kind, findings.filter((finding) => finding.kind === kind).length]));
301
+ await testInfo.attach('page-quality.json', {
302
+ body: JSON.stringify({ pages: routes.length, findings: findings.length, byKind, details: findings }, null, 2),
303
+ contentType: 'application/json',
304
+ });
305
+ testInfo.annotations.push({
306
+ type: 'advisory',
307
+ description: `${findings.length} page-quality finding(s) across ${routes.length} page(s)${findings.length ? `: ${Object.entries(byKind).map(([kind, count]) => `${kind} ${count}`).join(', ')}` : ''}; see page-quality.json.`,
308
+ });
309
+ });
310
+ });
311
+ }
312
+ function inspectStructuredData(route, payloads, businessId) {
313
+ const failures = [];
314
+ if (payloads.some((payload) => payload.parseError))
315
+ failures.push(`${route.path}: invalid JSON-LD`);
316
+ if (route.indexing === 'index' && payloads.length === 0)
317
+ failures.push(`${route.path}: no JSON-LD`);
318
+ const ids = [];
319
+ let referencesBusiness = false;
320
+ const visit = (value) => {
321
+ if (!value || typeof value !== 'object')
322
+ return;
323
+ if (Array.isArray(value)) {
324
+ value.forEach(visit);
325
+ return;
326
+ }
327
+ const node = value;
328
+ const types = [].concat(node['@type'] ?? []).map(String);
329
+ if (typeof node['@id'] === 'string') {
330
+ if (node['@id'] === businessId)
331
+ referencesBusiness = true;
332
+ if (types.length > 0)
333
+ ids.push(node['@id']);
334
+ }
335
+ if (businessId && types.some((type) => BUSINESS_TYPES.has(type)) && node['@id'] !== businessId) {
336
+ failures.push(`${route.path}: business node @id ${String(node['@id'] ?? 'missing')} (expected ${businessId})`);
337
+ }
338
+ Object.values(node).forEach(visit);
339
+ };
340
+ payloads.forEach(visit);
341
+ const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
342
+ if (duplicate)
343
+ failures.push(`${route.path}: JSON-LD repeats node ${duplicate}`);
344
+ if (businessId && route.indexing === 'index' && payloads.length > 0 && !referencesBusiness)
345
+ failures.push(`${route.path}: JSON-LD does not reference ${businessId}`);
346
+ return failures;
347
+ }
348
+ function extractLinks(html) {
349
+ return [...html.matchAll(/<a\b[^>]*\bhref=["']([^"']+)["']/gi)].map((match) => decodeXml(match[1]));
350
+ }
351
+ function internalPath(href, base, origin) {
352
+ if (/^(?:mailto|tel|sms|javascript):/i.test(href) || href.startsWith('#'))
353
+ return null;
354
+ let url;
355
+ try {
356
+ url = new URL(href, base);
357
+ }
358
+ catch {
359
+ return null;
360
+ }
361
+ if (url.origin !== origin && !['127.0.0.1', 'localhost'].includes(url.hostname))
362
+ return null;
363
+ if (ASSET_PATH.test(url.pathname) || url.pathname.startsWith('/api/') || url.pathname.startsWith('/_next/'))
364
+ return null;
365
+ return normalisePath(url.pathname);
366
+ }
367
+ function normalisePath(path) {
368
+ const pathname = path.split(/[?#]/)[0] || '/';
369
+ return pathname.length > 1 ? pathname.replace(/\/+$/, '') : '/';
370
+ }
371
+ /**
372
+ * Absolute (`https://host`) and protocol-relative (`//host`) destinations leave
373
+ * the site; browsers also read `/\host` and `\\host` as protocol-relative.
374
+ */
375
+ export function isExternalDestination(destination) {
376
+ return /^(?:[a-z][a-z0-9+.-]*:)?[\\/]{2}/i.test(destination.trim());
377
+ }
378
+ function decodeXml(value) {
379
+ return value.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
380
+ }
381
+ function escapeRegExp(value) {
382
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
383
+ }
384
+ async function eachWithConcurrency(items, limit, task) {
385
+ const queue = [...items];
386
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, queue.length)) }, async () => {
387
+ for (let item = queue.shift(); item !== undefined; item = queue.shift())
388
+ await task(item);
389
+ }));
390
+ }
391
+ //# sourceMappingURL=estate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estate.js","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared technical-estate journey for a Brandlab production build:\n *\n * // tests/e2e/estate.spec.ts\n * import { defineBrandlabEstateTests } from '@brandlab-digital/conformance/estate'\n * import { site } from '../../src/config/site'\n * import { routes } from '../../src/data/routes'\n * import { redirects } from '../../src/data/redirects'\n *\n * defineBrandlabEstateTests({ site, routes, redirects })\n *\n * It asserts the estate and its routes: the sitemap, each route's status,\n * canonical and indexing, internal links, redirects, unknown slugs and\n * robots.txt. Page and structured-data quality is reported, never enforced,\n * by defineBrandlabPageQualityTests: a backend release keeps production's pages\n * as they are, which the comparison harness proves. Counts are always derived\n * from the current catalogue, never a historic constant.\n */\nimport { expect, test, type APIRequestContext } from '@playwright/test';\nimport { extractSearchCertificationHtmlSnapshot } from './search-certification.js';\n\nexport interface BrandlabEstateRoute {\n path: string;\n canonicalPath?: string;\n indexing: 'index' | 'noindex-follow' | 'noindex-nofollow';\n}\n\nexport interface BrandlabEstateRedirect {\n source: string;\n destination: string;\n status: 301 | 302 | 303 | 307 | 308;\n /** Host-scoped alias rules are proved by the comparison harness host probes. */\n host?: string;\n}\n\nexport interface BrandlabEstateTestOptions {\n site: { canonicalUrl: string | URL };\n /** The route catalogue. A `/404` route is Next's not-found page and must answer 404. */\n routes: readonly BrandlabEstateRoute[];\n /**\n * The redirect table. A source with parameters (`/old/:slug`, `:path*`) is\n * requested with one sample segment per parameter and checked against its\n * destination filled in the same way; a parameter with its own regular\n * expression cannot be sampled and is listed as skipped.\n */\n redirects?: readonly BrandlabEstateRedirect[];\n /** Paths that must answer 404; defaults to one unknown slug under every catalogue parent. */\n notFoundPaths?: readonly string[];\n /** Also prove `<source>/` redirects straight to the destination (the shared helper emits these). */\n trailingSlashRedirects?: boolean;\n concurrency?: number;\n timeout?: number;\n}\n\nexport interface BrandlabPageQualityTestOptions {\n site: { canonicalUrl: string | URL };\n routes: readonly BrandlabEstateRoute[];\n /** The @id every indexable page's graph references; defaults to `<origin>/#business`, `false` skips it. */\n businessId?: string | false;\n concurrency?: number;\n timeout?: number;\n}\n\nexport interface BrandlabPageQualityFinding {\n path: string;\n kind: 'headings' | 'title' | 'description' | 'og-url' | 'implementation-wording' | 'structured-data';\n detail: string;\n}\n\nconst BUSINESS_TYPES = new Set([\n 'LocalBusiness', 'HomeAndConstructionBusiness', 'GeneralContractor', 'Plumber', 'Electrician',\n 'RoofingContractor', 'HousePainter', 'Locksmith', 'MovingCompany', 'HVACBusiness', 'ProfessionalService',\n]);\nconst UNKNOWN_SLUG = 'brandlab-estate-unknown-page';\nconst NOT_FOUND_ROUTE = '/404';\nconst PATTERN_SAMPLE = 'brandlab-estate-sample';\nconst ASSET_PATH = /\\.[a-z0-9]{2,5}$/i;\n\nexport function defineBrandlabEstateTests(options: BrandlabEstateTestOptions) {\n const origin = new URL(String(options.site.canonicalUrl)).origin;\n const canonical = (path: string) => new URL(path, `${origin}/`).href;\n const routes = options.routes;\n const knownPaths = new Set(routes.map((route) => normalisePath(route.path)));\n const redirects = (options.redirects ?? []).filter((rule) => !rule.host);\n const redirectBySource = new Map(\n redirects.filter((rule) => !isRedirectPattern(rule.source)).map((rule) => [normalisePath(rule.source), rule]),\n );\n const patternRedirects = redirects.flatMap((rule) => {\n const matcher = isRedirectPattern(rule.source) ? redirectSourceMatcher(rule.source) : undefined;\n return matcher ? [{ rule, matcher }] : [];\n });\n const findRedirect = (path: string) =>\n redirectBySource.get(path) ?? patternRedirects.find(({ matcher }) => matcher.test(path))?.rule;\n const timeout = options.timeout ?? 240_000;\n\n test.describe('Brandlab technical estate', () => {\n test('the sitemap lists exactly the indexable catalogue routes', async ({ request }) => {\n test.setTimeout(timeout);\n const listed = await readSitemap(request, '/sitemap.xml', origin);\n const expected = routes\n .filter((route) => route.indexing === 'index')\n .map((route) => canonical(route.canonicalPath ?? route.path));\n expect(listed.length, 'the sitemap repeats a URL').toBe(new Set(listed).size);\n expect([...listed].sort()).toEqual([...new Set(expected)].sort());\n });\n\n test('every catalogue route serves its canonical identity and links only to live pages', async ({ request }, testInfo) => {\n test.setTimeout(timeout);\n const failures: string[] = [];\n const internalLinks = new Map<string, string>();\n const checked: { path: string; canonical: string | null; jsonLd: number }[] = [];\n await eachWithConcurrency(routes, options.concurrency ?? 8, async (route) => {\n const [get, head] = await Promise.all([\n request.get(route.path, { maxRedirects: 0 }),\n request.head(route.path, { maxRedirects: 0 }),\n ]);\n // Next serves its not-found page at /404 with a 404 status.\n const notFound = normalisePath(route.path) === NOT_FOUND_ROUTE;\n const expectedStatus = notFound ? 404 : 200;\n if (get.status() !== expectedStatus || head.status() !== expectedStatus) {\n failures.push(`${route.path}: GET ${get.status()}, HEAD ${head.status()} (expected ${expectedStatus})`);\n return;\n }\n const html = await get.text();\n const snapshot = extractSearchCertificationHtmlSnapshot(html);\n const robots = `${snapshot.robots ?? ''} ${get.headers()['x-robots-tag'] ?? ''}`;\n if (notFound) {\n if (!/noindex/i.test(robots)) failures.push(`${route.path}: the not-found page is indexable`);\n checked.push({ path: route.path, canonical: snapshot.canonical, jsonLd: snapshot.jsonLd.length });\n return;\n }\n const expectedCanonical = canonical(route.canonicalPath ?? route.path);\n const canonicalTags = html.match(/<link\\b[^>]*\\brel=[\"']canonical[\"'][^>]*>/gi) ?? [];\n if (canonicalTags.length !== 1) failures.push(`${route.path}: ${canonicalTags.length} canonical links`);\n if (!snapshot.canonical || new URL(snapshot.canonical, origin).href !== expectedCanonical) {\n failures.push(`${route.path}: canonical ${snapshot.canonical ?? 'missing'} (expected ${expectedCanonical})`);\n }\n if (route.indexing === 'index' && /noindex/i.test(robots)) failures.push(`${route.path}: indexable route is noindex`);\n if (route.indexing !== 'index' && !/noindex/i.test(robots)) failures.push(`${route.path}: ${route.indexing} route is indexable`);\n for (const href of extractLinks(html)) {\n const path = internalPath(href, expectedCanonical, origin);\n if (path && !internalLinks.has(path)) internalLinks.set(path, route.path);\n }\n checked.push({ path: route.path, canonical: snapshot.canonical, jsonLd: snapshot.jsonLd.length });\n });\n for (const [path, from] of [...internalLinks].sort()) {\n if (knownPaths.has(path)) continue;\n const redirect = findRedirect(path);\n if (redirect && isExternalDestination(redirect.destination)) continue;\n failures.push(redirect\n ? `${from}: links to ${path}, which redirects; link to ${redirect.destination} directly`\n : `${from}: links to ${path}, which is not a catalogue route`);\n }\n await testInfo.attach('technical-estate.json', {\n body: JSON.stringify({ routes: checked.sort((a, b) => a.path.localeCompare(b.path)), internalLinks: [...internalLinks.keys()].sort() }, null, 2),\n contentType: 'application/json',\n });\n expect(failures).toEqual([]);\n });\n\n test('every redirect resolves in one hop and keeps the query string', async ({ request }, testInfo) => {\n test.setTimeout(timeout);\n const failures: string[] = [];\n const skipped: string[] = [];\n for (const rule of redirects) {\n const probe = redirectProbe(rule);\n if (!probe) {\n skipped.push(`${rule.source} → ${rule.destination}`);\n continue;\n }\n const sources = [probe.source, ...(options.trailingSlashRedirects === false || probe.source === '/' || probe.source.endsWith('/') ? [] : [`${probe.source}/`])];\n for (const source of sources) {\n for (const method of ['GET', 'HEAD'] as const) {\n const response = await request.fetch(`${source}?brandlab_estate=1`, { method, maxRedirects: 0 });\n const location = response.headers().location;\n const expectedStatus: number[] = source === probe.source ? [rule.status] : [301, 308];\n if (!expectedStatus.includes(response.status()) || !location) {\n failures.push(`${method} ${source}: ${response.status()} (expected ${expectedStatus.join(' or ')} to ${probe.destination})`);\n continue;\n }\n const target = new URL(location, origin);\n if (isExternalDestination(probe.destination)) {\n if (!target.href.startsWith(new URL(probe.destination, origin).href.replace(/\\/$/, ''))) failures.push(`${method} ${source}: Location ${location} (expected ${probe.destination})`);\n continue;\n }\n if (target.pathname !== new URL(probe.destination, origin).pathname) failures.push(`${method} ${source}: Location ${location} (expected ${probe.destination})`);\n if (target.searchParams.get('brandlab_estate') !== '1') failures.push(`${method} ${source}: the query string was dropped`);\n }\n }\n // A destination with parameters names many pages; the sample fills it with a page that does not exist.\n if (!isExternalDestination(probe.destination) && !probe.sampled) {\n const destination = await request.get(probe.destination, { maxRedirects: 0 });\n if (destination.status() !== 200) failures.push(`${rule.source}: destination ${probe.destination} answers ${destination.status()} (a chain or a missing page)`);\n }\n }\n if (skipped.length > 0) {\n testInfo.annotations.push({ type: 'skipped-redirects', description: `${skipped.length} redirect(s) with a parameter pattern that cannot be sampled: ${skipped.join('; ')}` });\n }\n expect(failures).toEqual([]);\n });\n\n test('unknown slugs answer 404 instead of inventing pages', async ({ request }) => {\n const paths = options.notFoundPaths ?? defaultNotFoundPaths(routes);\n const failures: string[] = [];\n for (const path of paths) {\n const response = await request.get(path, { maxRedirects: 0 });\n if (response.status() !== 404) failures.push(`${path}: ${response.status()} (expected 404)`);\n }\n expect(failures).toEqual([]);\n });\n\n test('robots.txt allows crawling and names the sitemap', async ({ request }) => {\n const response = await request.get('/robots.txt');\n expect(response.status()).toBe(200);\n const body = await response.text();\n expect(body).toMatch(new RegExp(`^sitemap:\\\\s*${escapeRegExp(`${origin}/sitemap.xml`)}\\\\s*$`, 'im'));\n expect(body).not.toMatch(/^disallow:\\s*\\/\\s*$/im);\n });\n });\n}\n\n/**\n * Every `<loc>` in a sitemap, following one level of sitemap index. Nothing is\n * followed through a redirect, and with `origin` every nested sitemap must be\n * on that origin (it is then fetched from the build under test).\n */\nexport async function readSitemap(request: APIRequestContext, path: string, origin?: string): Promise<string[]> {\n const read = async (target: string) => {\n const response = await request.get(target, { maxRedirects: 0 });\n if (response.status() !== 200) throw new Error(`${target} answers ${response.status()}; a sitemap must answer 200 without redirecting.`);\n return response.text();\n };\n const xml = await read(path);\n const locations = extractSitemapLocations(xml);\n if (!/<sitemapindex\\b/i.test(xml)) return locations;\n const nested: string[] = [];\n for (const location of locations) {\n const url = new URL(location);\n if (origin && url.origin !== new URL(origin).origin) {\n throw new Error(`The sitemap index lists ${location}, which is not on ${new URL(origin).origin}.`);\n }\n nested.push(...extractSitemapLocations(await read(`${url.pathname}${url.search}`)));\n }\n return nested;\n}\n\nexport function extractSitemapLocations(xml: string): string[] {\n return [...xml.matchAll(/<loc>\\s*([^<\\s]+)\\s*<\\/loc>/gi)].map((match) =>\n new URL(decodeXml(match[1]!)).href);\n}\n\nexport function defaultNotFoundPaths(routes: readonly BrandlabEstateRoute[]): string[] {\n const parents = new Set(['']);\n for (const route of routes) {\n const segments = normalisePath(route.path).split('/').filter(Boolean);\n if (segments.length > 1) parents.add(`/${segments.slice(0, -1).join('/')}`);\n if (segments.length === 1) parents.add(`/${segments[0]}`);\n }\n return [...parents].sort().map((parent) => `${parent}/${UNKNOWN_SLUG}`);\n}\n\n/** A Next redirect path with parameters: `:name`, `:name*`, `:name+`, `:name?` or a regular-expression group. */\nexport function isRedirectPattern(path: string): boolean {\n return /:[A-Za-z_]\\w*|\\(/.test(path.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, ''));\n}\n\n/**\n * The request that proves a redirect rule: a pattern gets one sample segment\n * per parameter, in its source and destination alike. Undefined when a\n * parameter carries its own regular expression, which no sample can satisfy\n * with certainty.\n */\nexport function redirectProbe(rule: Pick<BrandlabEstateRedirect, 'source' | 'destination'>): { source: string; destination: string; sampled: boolean } | undefined {\n const sample = (path: string) => {\n if (!isRedirectPattern(path)) return path;\n // Keep an external destination's scheme and host; parameters live in the path.\n const origin = /^[a-z][a-z0-9+.-]*:\\/\\/[^/]*/i.exec(path)?.[0] ?? '';\n const rest = path.slice(origin.length);\n if (rest.includes('(')) return undefined;\n return `${origin}${rest.replace(/:[A-Za-z_]\\w*[*+?]?/g, PATTERN_SAMPLE)}`;\n };\n const source = sample(rule.source);\n const destination = sample(rule.destination);\n if (source === undefined || destination === undefined) return undefined;\n return { source, destination, sampled: isRedirectPattern(rule.destination) };\n}\n\n/** A matcher for paths a pattern source redirects, for internal-link checks. */\nfunction redirectSourceMatcher(source: string): RegExp | undefined {\n if (source.includes('(')) return undefined;\n const pattern = normalisePath(source).split(/(:[A-Za-z_]\\w*[*+?]?)/).map((part) => {\n const parameter = /^:[A-Za-z_]\\w*([*+?]?)$/.exec(part);\n if (!parameter) return escapeRegExp(part);\n return parameter[1] === '*' || parameter[1] === '+' ? '.+' : parameter[1] === '?' ? '[^/]*' : '[^/]+';\n }).join('');\n return new RegExp(`^${pattern}$`);\n}\n\n/**\n * The advisory page-quality journey: headings, title, description, og:url,\n * implementation wording and structured-data identity for every catalogue\n * page. It reports what it finds (page-quality.json and an annotation) and\n * never fails, because a backend release keeps production's pages as they\n * are; the comparison harness proves them unchanged.\n *\n * // tests/e2e/page-quality.spec.ts\n * defineBrandlabPageQualityTests({ site, routes })\n */\nexport function defineBrandlabPageQualityTests(options: BrandlabPageQualityTestOptions) {\n const origin = new URL(String(options.site.canonicalUrl)).origin;\n const canonical = (path: string) => new URL(path, `${origin}/`).href;\n const businessId = options.businessId ?? `${origin}/#business`;\n const timeout = options.timeout ?? 240_000;\n const routes = options.routes.filter((route) => normalisePath(route.path) !== NOT_FOUND_ROUTE);\n\n test.describe('Brandlab page quality (advisory)', () => {\n test('reports page and structured-data quality without failing', async ({ request }, testInfo) => {\n test.setTimeout(timeout);\n const findings: BrandlabPageQualityFinding[] = [];\n await eachWithConcurrency(routes, options.concurrency ?? 8, async (route) => {\n const response = await request.get(route.path, { maxRedirects: 0 });\n // The estate journey owns status codes; quality is read from the pages that load.\n if (response.status() !== 200) return;\n const html = await response.text();\n const snapshot = extractSearchCertificationHtmlSnapshot(html);\n const add = (kind: BrandlabPageQualityFinding['kind'], detail: string) => findings.push({ path: route.path, kind, detail });\n const headings = html.match(/<h1(?:\\s|>)/gi)?.length ?? 0;\n if (headings !== 1) add('headings', `${headings} h1 headings`);\n if (!snapshot.title) add('title', 'no title');\n if (!snapshot.description) add('description', 'no meta description');\n const expectedCanonical = canonical(route.canonicalPath ?? route.path);\n const ogUrl = snapshot.openGraph['og:url'];\n if (ogUrl && new URL(ogUrl, origin).href !== expectedCanonical) add('og-url', `og:url ${ogUrl} is not the canonical URL`);\n if (snapshot.implementationCopyMatches.length > 0) add('implementation-wording', snapshot.implementationCopyMatches.join(', '));\n for (const failure of inspectStructuredData(route, snapshot.jsonLd, businessId)) {\n add('structured-data', failure.slice(route.path.length + 2));\n }\n });\n findings.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));\n const byKind = Object.fromEntries(\n [...new Set(findings.map((finding) => finding.kind))].sort().map((kind) => [kind, findings.filter((finding) => finding.kind === kind).length]),\n );\n await testInfo.attach('page-quality.json', {\n body: JSON.stringify({ pages: routes.length, findings: findings.length, byKind, details: findings }, null, 2),\n contentType: 'application/json',\n });\n testInfo.annotations.push({\n type: 'advisory',\n description: `${findings.length} page-quality finding(s) across ${routes.length} page(s)${findings.length ? `: ${Object.entries(byKind).map(([kind, count]) => `${kind} ${count}`).join(', ')}` : ''}; see page-quality.json.`,\n });\n });\n });\n}\n\nfunction inspectStructuredData(route: BrandlabEstateRoute, payloads: readonly unknown[], businessId: string | false): string[] {\n const failures: string[] = [];\n if (payloads.some((payload) => (payload as { parseError?: boolean }).parseError)) failures.push(`${route.path}: invalid JSON-LD`);\n if (route.indexing === 'index' && payloads.length === 0) failures.push(`${route.path}: no JSON-LD`);\n const ids: string[] = [];\n let referencesBusiness = false;\n const visit = (value: unknown) => {\n if (!value || typeof value !== 'object') return;\n if (Array.isArray(value)) { value.forEach(visit); return; }\n const node = value as Record<string, unknown>;\n const types = ([] as unknown[]).concat(node['@type'] ?? []).map(String);\n if (typeof node['@id'] === 'string') {\n if (node['@id'] === businessId) referencesBusiness = true;\n if (types.length > 0) ids.push(node['@id']);\n }\n if (businessId && types.some((type) => BUSINESS_TYPES.has(type)) && node['@id'] !== businessId) {\n failures.push(`${route.path}: business node @id ${String(node['@id'] ?? 'missing')} (expected ${businessId})`);\n }\n Object.values(node).forEach(visit);\n };\n payloads.forEach(visit);\n const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);\n if (duplicate) failures.push(`${route.path}: JSON-LD repeats node ${duplicate}`);\n if (businessId && route.indexing === 'index' && payloads.length > 0 && !referencesBusiness) failures.push(`${route.path}: JSON-LD does not reference ${businessId}`);\n return failures;\n}\n\nfunction extractLinks(html: string): string[] {\n return [...html.matchAll(/<a\\b[^>]*\\bhref=[\"']([^\"']+)[\"']/gi)].map((match) => decodeXml(match[1]!));\n}\n\nfunction internalPath(href: string, base: string, origin: string): string | null {\n if (/^(?:mailto|tel|sms|javascript):/i.test(href) || href.startsWith('#')) return null;\n let url: URL;\n try { url = new URL(href, base); } catch { return null; }\n if (url.origin !== origin && !['127.0.0.1', 'localhost'].includes(url.hostname)) return null;\n if (ASSET_PATH.test(url.pathname) || url.pathname.startsWith('/api/') || url.pathname.startsWith('/_next/')) return null;\n return normalisePath(url.pathname);\n}\n\nfunction normalisePath(path: string): string {\n const pathname = path.split(/[?#]/)[0] || '/';\n return pathname.length > 1 ? pathname.replace(/\\/+$/, '') : '/';\n}\n\n/**\n * Absolute (`https://host`) and protocol-relative (`//host`) destinations leave\n * the site; browsers also read `/\\host` and `\\\\host` as protocol-relative.\n */\nexport function isExternalDestination(destination: string): boolean {\n return /^(?:[a-z][a-z0-9+.-]*:)?[\\\\/]{2}/i.test(destination.trim());\n}\n\nfunction decodeXml(value: string): string {\n return value.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#39;/g, \"'\");\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nasync function eachWithConcurrency<T>(items: readonly T[], limit: number, task: (item: T) => Promise<void>) {\n const queue = [...items];\n await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, queue.length)) }, async () => {\n for (let item = queue.shift(); item !== undefined; item = queue.shift()) await task(item);\n }));\n}\n"]}
@@ -0,0 +1,45 @@
1
+ export interface BrandlabFamiliesTestOptions {
2
+ /** The route catalogue; each app route's page is the first path Next routes to it. */
3
+ routes: readonly {
4
+ path: string;
5
+ }[];
6
+ /** Pages for app routes the catalogue does not list, keyed by app route (for example `'/quote'`). */
7
+ samples?: Readonly<Record<string, string>>;
8
+ /** App routes with no page to check, each with the reason. */
9
+ skip?: Readonly<Record<string, string>>;
10
+ /** The production build directory, relative to the working directory (default `.next`). */
11
+ buildDir?: string;
12
+ /** Viewport widths (default 390 and 1440). */
13
+ widths?: readonly number[];
14
+ /** How long a document may take to load (DOMContentLoaded), default 45 seconds; a timeout is retried once. */
15
+ navigationTimeoutMs?: number;
16
+ /** How long to wait for the network to settle before checking, default 10 seconds; a busy network is not a failure. */
17
+ settleTimeoutMs?: number;
18
+ }
19
+ export interface BrandlabFamilyPage {
20
+ /** The app route, for example `/services/[slug]`. */
21
+ appRoute: string;
22
+ /** The page checked for it. */
23
+ path: string;
24
+ /** 200, or 404 for the not-found page. */
25
+ status: 200 | 404;
26
+ }
27
+ export declare const BRANDLAB_FAMILIES_WIDTHS: readonly number[];
28
+ /** Page app routes from `app-path-routes-manifest.json`, without Next's internal error pages. */
29
+ export declare function appPageRoutes(manifest: Readonly<Record<string, string>>): string[];
30
+ /** The app route Next serves a path from, by Next's precedence: static, then dynamic, then catch-all. */
31
+ export declare function resolveAppRoute(appRoutes: readonly string[], path: string): string | undefined;
32
+ /** One page per app route, and every reason the plan cannot cover the build. */
33
+ export declare function planBrandlabFamilyPages(input: {
34
+ manifest: Readonly<Record<string, string>>;
35
+ routes: readonly {
36
+ path: string;
37
+ }[];
38
+ samples?: Readonly<Record<string, string>>;
39
+ skip?: Readonly<Record<string, string>>;
40
+ }): {
41
+ pages: BrandlabFamilyPage[];
42
+ problems: string[];
43
+ };
44
+ export declare function defineBrandlabFamiliesTests(options: BrandlabFamiliesTestOptions): void;
45
+ //# sourceMappingURL=families-e2e.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"families-e2e.d.ts","sourceRoot":"","sources":["../src/families-e2e.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,2BAA2B;IAC1C,sFAAsF;IACtF,MAAM,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpC,qGAAqG;IACrG,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxC,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,8GAA8G;IAC9G,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uHAAuH;IACvH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;CACnB;AAED,eAAO,MAAM,wBAAwB,mBAA6B,CAAC;AA+CnE,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,EAAE,CAQlF;AAED,yGAAyG;AACzG,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ9F;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,MAAM,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACzC,GAAG;IAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAsCtD;AA4CD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,2BAA2B,QA4E/E"}