@brandlab-digital/conformance 2.1.0 → 3.1.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/dist/analytics-e2e.d.ts +52 -0
- package/dist/analytics-e2e.d.ts.map +1 -0
- package/dist/analytics-e2e.js +189 -0
- package/dist/analytics-e2e.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/contact-e2e.d.ts +43 -0
- package/dist/contact-e2e.d.ts.map +1 -0
- package/dist/contact-e2e.js +193 -0
- package/dist/contact-e2e.js.map +1 -0
- package/dist/current-release.js +7 -7
- package/dist/current-release.js.map +1 -1
- package/dist/e2e-support.d.ts +42 -0
- package/dist/e2e-support.d.ts.map +1 -0
- package/dist/e2e-support.js +126 -0
- package/dist/e2e-support.js.map +1 -0
- package/dist/engineering.d.ts +14 -0
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +446 -50
- package/dist/engineering.js.map +1 -1
- package/dist/estate.d.ts +105 -0
- package/dist/estate.d.ts.map +1 -0
- package/dist/estate.js +391 -0
- package/dist/estate.js.map +1 -0
- package/dist/families-e2e.d.ts +45 -0
- package/dist/families-e2e.d.ts.map +1 -0
- package/dist/families-e2e.js +248 -0
- package/dist/families-e2e.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +138 -0
- package/dist/glob.js.map +1 -0
- package/dist/knip.d.ts +47 -0
- package/dist/knip.d.ts.map +1 -0
- package/dist/knip.js +84 -0
- package/dist/knip.js.map +1 -0
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +163 -147
- package/dist/project.js.map +1 -1
- package/dist/public-copy.d.ts +36 -0
- package/dist/public-copy.d.ts.map +1 -0
- package/dist/public-copy.js +517 -0
- package/dist/public-copy.js.map +1 -0
- package/dist/rule-registry.d.ts +292 -0
- package/dist/rule-registry.d.ts.map +1 -0
- package/dist/rule-registry.js +68 -0
- package/dist/rule-registry.js.map +1 -0
- package/dist/search-certification.d.ts +4 -0
- package/dist/search-certification.d.ts.map +1 -1
- package/dist/search-certification.js +17 -3
- package/dist/search-certification.js.map +1 -1
- package/dist/source-files.d.ts +54 -0
- package/dist/source-files.d.ts.map +1 -0
- package/dist/source-files.js +323 -0
- package/dist/source-files.js.map +1 -0
- package/dist/static-rules.d.ts +45 -0
- package/dist/static-rules.d.ts.map +1 -0
- package/dist/static-rules.js +762 -0
- package/dist/static-rules.js.map +1 -0
- package/dist/verification-policy.d.ts +13 -4
- package/dist/verification-policy.d.ts.map +1 -1
- package/dist/verification-policy.js +31 -12
- package/dist/verification-policy.js.map +1 -1
- package/package.json +37 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared analytics journeys for a Brandlab production build. Google is
|
|
3
|
+
* never contacted: the tag loader is stubbed and every dataLayer command is
|
|
4
|
+
* recorded in the test process, across navigations.
|
|
5
|
+
*
|
|
6
|
+
* // tests/e2e/analytics.spec.ts
|
|
7
|
+
* import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'
|
|
8
|
+
*
|
|
9
|
+
* defineBrandlabAnalyticsTests({
|
|
10
|
+
* paths: ['/', '/services/blocked-drains'],
|
|
11
|
+
* contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)
|
|
15
|
+
* so BrandlabGoogleTag renders.
|
|
16
|
+
*/
|
|
17
|
+
import { type Page } from '@playwright/test';
|
|
18
|
+
import { type BrandlabContactJourney } from './contact-e2e.js';
|
|
19
|
+
export interface BrandlabAnalyticsTestOptions {
|
|
20
|
+
/** Pages to load; the first renders a telephone link. Defaults to ['/']. */
|
|
21
|
+
paths?: readonly string[];
|
|
22
|
+
/** Selector of the telephone link to tap; defaults to the first visible tel: link. */
|
|
23
|
+
phoneLink?: string;
|
|
24
|
+
/** The contact journey whose accepted result emits one contact_form. */
|
|
25
|
+
contact?: BrandlabContactJourney & {
|
|
26
|
+
retry?: string | RegExp;
|
|
27
|
+
};
|
|
28
|
+
/** Event names the project's analytics contract declares in additionalEvents. */
|
|
29
|
+
additionalEvents?: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */
|
|
32
|
+
export declare const BRANDLAB_RETIRED_ANALYTICS_EVENTS: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,
|
|
35
|
+
* CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The
|
|
36
|
+
* Google Ads `conversion` event is also allowed, only with an AW- destination.
|
|
37
|
+
*/
|
|
38
|
+
export declare const BRANDLAB_ANALYTICS_EVENTS: readonly string[];
|
|
39
|
+
export type AnalyticsCommand = unknown[];
|
|
40
|
+
/** Record every gtag/dataLayer command the page pushes, surviving navigations. */
|
|
41
|
+
export declare function recordAnalyticsCommands(page: Page): Promise<AnalyticsCommand[]>;
|
|
42
|
+
export declare function analyticsEvents(commands: readonly AnalyticsCommand[], name?: string): {
|
|
43
|
+
name: string;
|
|
44
|
+
params: Record<string, unknown>;
|
|
45
|
+
}[];
|
|
46
|
+
/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */
|
|
47
|
+
export declare function analyticsEventProblems(commands: readonly AnalyticsCommand[], options?: {
|
|
48
|
+
enteredValues?: readonly string[];
|
|
49
|
+
additionalEvents?: readonly string[];
|
|
50
|
+
}): string[];
|
|
51
|
+
export declare function defineBrandlabAnalyticsTests(options?: BrandlabAnalyticsTestOptions): void;
|
|
52
|
+
//# sourceMappingURL=analytics-e2e.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-e2e.d.ts","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAA2C,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAQxG,MAAM,WAAW,4BAA4B;IAC3C,4EAA4E;IAC5E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,sBAAsB,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/D,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,2GAA2G;AAC3G,eAAO,MAAM,iCAAiC,mBAE5C,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,mBAEpC,CAAC;AAKH,MAAM,MAAM,gBAAgB,GAAG,OAAO,EAAE,CAAC;AAEzC,kFAAkF;AAClF,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA+BrF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;UAGvC,MAAM;YAAgC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzG;AAED,gGAAgG;AAChG,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EACrC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GACxF,MAAM,EAAE,CAiBV;AAED,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,4BAAiC,QA4FtF"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared analytics journeys for a Brandlab production build. Google is
|
|
3
|
+
* never contacted: the tag loader is stubbed and every dataLayer command is
|
|
4
|
+
* recorded in the test process, across navigations.
|
|
5
|
+
*
|
|
6
|
+
* // tests/e2e/analytics.spec.ts
|
|
7
|
+
* import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'
|
|
8
|
+
*
|
|
9
|
+
* defineBrandlabAnalyticsTests({
|
|
10
|
+
* paths: ['/', '/services/blocked-drains'],
|
|
11
|
+
* contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)
|
|
15
|
+
* so BrandlabGoogleTag renders.
|
|
16
|
+
*/
|
|
17
|
+
import { expect, test } from '@playwright/test';
|
|
18
|
+
import { fillBrandlabContactForm, successPattern } from './contact-e2e.js';
|
|
19
|
+
import { ACCEPTED_CONTACT_RESULT, RETRYABLE_CONTACT_FAILURE, contactEndpointMatcher, isolateFromNetwork, } from './e2e-support.js';
|
|
20
|
+
/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */
|
|
21
|
+
export const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([
|
|
22
|
+
'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,
|
|
26
|
+
* CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The
|
|
27
|
+
* Google Ads `conversion` event is also allowed, only with an AW- destination.
|
|
28
|
+
*/
|
|
29
|
+
export const BRANDLAB_ANALYTICS_EVENTS = Object.freeze([
|
|
30
|
+
'contact_form', 'phone_link_click', 'lead_form_start', 'lead_form_error', 'lead_cta_click',
|
|
31
|
+
]);
|
|
32
|
+
const PII_PARAMETER = /^(?:e_?mail|email_address|user_email|contact_email|phone|phone_number|mobile|mobile_number|user_phone|name|first_name|last_name|full_name|customer_name|contact_name|message|enquiry_message|form_message|address|street_address|residential_address|attachment|attachment_name|file_name|filename)$/i;
|
|
33
|
+
const PHONE_DEDUPE_WINDOW_MS = 1_200;
|
|
34
|
+
/** Record every gtag/dataLayer command the page pushes, surviving navigations. */
|
|
35
|
+
export async function recordAnalyticsCommands(page) {
|
|
36
|
+
const commands = [];
|
|
37
|
+
await page.exposeFunction('__brandlabRecordAnalytics', (command) => { commands.push(command); });
|
|
38
|
+
await page.addInitScript(() => {
|
|
39
|
+
const record = (entry) => {
|
|
40
|
+
try {
|
|
41
|
+
const command = entry && typeof entry === 'object' && 'length' in entry
|
|
42
|
+
? Array.from(entry)
|
|
43
|
+
: [entry];
|
|
44
|
+
void window
|
|
45
|
+
.__brandlabRecordAnalytics(JSON.parse(JSON.stringify(command)));
|
|
46
|
+
}
|
|
47
|
+
catch { /* an unserialisable command is not analytics evidence */ }
|
|
48
|
+
};
|
|
49
|
+
const wrapped = new WeakSet();
|
|
50
|
+
const wrap = (layer) => {
|
|
51
|
+
if (wrapped.has(layer))
|
|
52
|
+
return layer;
|
|
53
|
+
wrapped.add(layer);
|
|
54
|
+
const push = layer.push.bind(layer);
|
|
55
|
+
layer.push = (...entries) => { entries.forEach(record); return push(...entries); };
|
|
56
|
+
layer.forEach(record);
|
|
57
|
+
return layer;
|
|
58
|
+
};
|
|
59
|
+
let dataLayer = wrap([]);
|
|
60
|
+
Object.defineProperty(window, 'dataLayer', {
|
|
61
|
+
configurable: true,
|
|
62
|
+
get: () => dataLayer,
|
|
63
|
+
// `window.dataLayer = window.dataLayer || []` reassigns the same array.
|
|
64
|
+
set: (value) => { if (Array.isArray(value))
|
|
65
|
+
dataLayer = wrap(value); },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
return commands;
|
|
69
|
+
}
|
|
70
|
+
export function analyticsEvents(commands, name) {
|
|
71
|
+
return commands
|
|
72
|
+
.filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))
|
|
73
|
+
.map((command) => ({ name: command[1], params: (command[2] ?? {}) }));
|
|
74
|
+
}
|
|
75
|
+
/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */
|
|
76
|
+
export function analyticsEventProblems(commands, options = {}) {
|
|
77
|
+
const problems = [];
|
|
78
|
+
const allowed = new Set([...BRANDLAB_ANALYTICS_EVENTS, ...(options.additionalEvents ?? [])]);
|
|
79
|
+
for (const event of analyticsEvents(commands)) {
|
|
80
|
+
if (BRANDLAB_RETIRED_ANALYTICS_EVENTS.includes(event.name))
|
|
81
|
+
problems.push(`retired event ${event.name}`);
|
|
82
|
+
else if (event.name === 'conversion') {
|
|
83
|
+
if (!String(event.params.send_to ?? '').startsWith('AW-'))
|
|
84
|
+
problems.push('conversion event without a Google Ads destination');
|
|
85
|
+
}
|
|
86
|
+
else if (!allowed.has(event.name))
|
|
87
|
+
problems.push(`unexpected event ${event.name}`);
|
|
88
|
+
for (const key of Object.keys(event.params)) {
|
|
89
|
+
if (PII_PARAMETER.test(key))
|
|
90
|
+
problems.push(`${event.name} carries the ${key} parameter`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const serialised = JSON.stringify(commands).toLowerCase();
|
|
94
|
+
for (const value of (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5)) {
|
|
95
|
+
if (serialised.includes(value.toLowerCase()))
|
|
96
|
+
problems.push(`an analytics command contains the entered value "${value}"`);
|
|
97
|
+
}
|
|
98
|
+
return problems;
|
|
99
|
+
}
|
|
100
|
+
export function defineBrandlabAnalyticsTests(options = {}) {
|
|
101
|
+
const paths = options.paths?.length ? options.paths : ['/'];
|
|
102
|
+
const additionalEvents = options.additionalEvents ?? [];
|
|
103
|
+
test.describe('Brandlab analytics', () => {
|
|
104
|
+
// Service workers could answer requests outside page and context routing.
|
|
105
|
+
test.use({ serviceWorkers: 'block' });
|
|
106
|
+
let commands = [];
|
|
107
|
+
test.beforeEach(async ({ page, baseURL }) => {
|
|
108
|
+
await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });
|
|
109
|
+
commands = await recordAnalyticsCommands(page);
|
|
110
|
+
});
|
|
111
|
+
test('renders exactly one Google tag on every page', async ({ page }) => {
|
|
112
|
+
for (const path of paths) {
|
|
113
|
+
commands.length = 0;
|
|
114
|
+
await page.goto(path, { waitUntil: 'load' });
|
|
115
|
+
const loaders = page.locator('script[src*="googletagmanager.com/gtag/js"]');
|
|
116
|
+
await expect(loaders.first(), `${path}: no Google tag rendered (build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID)`)
|
|
117
|
+
.toBeAttached({ timeout: 15_000 });
|
|
118
|
+
expect(await loaders.count(), `${path}: renders exactly one Google tag loader`).toBe(1);
|
|
119
|
+
await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: gtag('js') runs once`).toBe(1);
|
|
120
|
+
const destinations = commands.filter((command) => command[0] === 'config').map((command) => String(command[1]));
|
|
121
|
+
expect(new Set(destinations).size, `${path}: a destination is configured twice`).toBe(destinations.length);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
test('sends one phone_link_click per tap', async ({ page }) => {
|
|
125
|
+
await page.goto(paths[0], { waitUntil: 'load' });
|
|
126
|
+
const link = options.phoneLink ? page.locator(options.phoneLink).first() : page.locator('a[href^="tel:"]:visible').first();
|
|
127
|
+
await expect(link, 'the page renders a telephone link').toBeVisible();
|
|
128
|
+
await expect.poll(() => commands.some((command) => command[0] === 'js')).toBe(true);
|
|
129
|
+
// Keep the page in place; the shared listener still sees the tap.
|
|
130
|
+
await page.evaluate(() => window.addEventListener('click', (event) => {
|
|
131
|
+
if (event.target?.closest?.('a[href^="tel:"]'))
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
}, true));
|
|
134
|
+
await link.click();
|
|
135
|
+
await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(1);
|
|
136
|
+
await page.waitForTimeout(PHONE_DEDUPE_WINDOW_MS + 300);
|
|
137
|
+
expect(analyticsEvents(commands, 'phone_link_click'), 'one tap sends one event').toHaveLength(1);
|
|
138
|
+
await link.click();
|
|
139
|
+
await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(2);
|
|
140
|
+
for (const event of analyticsEvents(commands, 'phone_link_click')) {
|
|
141
|
+
expect(event.params.placement, 'phone_link_click carries its placement').toBeTruthy();
|
|
142
|
+
}
|
|
143
|
+
expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
if (options.contact) {
|
|
146
|
+
const journey = options.contact;
|
|
147
|
+
test('sends one contact_form only after an accepted enquiry, without lead details', async ({ page }) => {
|
|
148
|
+
let attempts = 0;
|
|
149
|
+
await page.route(contactEndpointMatcher(journey.endpoint), async (route) => {
|
|
150
|
+
attempts += 1;
|
|
151
|
+
await route.fulfill(attempts === 1
|
|
152
|
+
? { status: 503, json: RETRYABLE_CONTACT_FAILURE }
|
|
153
|
+
: { status: 200, json: ACCEPTED_CONTACT_RESULT });
|
|
154
|
+
});
|
|
155
|
+
await fillBrandlabContactForm(page, journey);
|
|
156
|
+
const submit = page.locator('form').filter({ has: page.locator(`[name="${Object.keys(journey.fields)[0]}"]`) })
|
|
157
|
+
.first().getByRole('button', { name: journey.submit, exact: typeof journey.submit === 'string' });
|
|
158
|
+
await submit.click();
|
|
159
|
+
await expect.poll(() => attempts).toBe(1);
|
|
160
|
+
await page.waitForTimeout(500);
|
|
161
|
+
expect(analyticsEvents(commands, 'contact_form'), 'a failed delivery is not a conversion').toHaveLength(0);
|
|
162
|
+
const retry = journey.retry ?? journey.submit;
|
|
163
|
+
await page.locator('form').filter({ has: page.locator(`[name="${Object.keys(journey.fields)[0]}"]`) })
|
|
164
|
+
.first().getByRole('button', { name: retry, exact: typeof retry === 'string' }).click();
|
|
165
|
+
await expect(page).toHaveURL(successPattern(journey.successPath));
|
|
166
|
+
await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);
|
|
167
|
+
await page.waitForTimeout(500);
|
|
168
|
+
expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);
|
|
169
|
+
expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents })).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
test('sends no retired or web_vital events', async ({ page }) => {
|
|
173
|
+
for (const path of paths) {
|
|
174
|
+
await page.goto(path, { waitUntil: 'load' });
|
|
175
|
+
await page.mouse.wheel(0, 1_200);
|
|
176
|
+
await page.waitForTimeout(300);
|
|
177
|
+
// Web-vitals reporters flush when the page is hidden.
|
|
178
|
+
await page.evaluate(() => {
|
|
179
|
+
Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });
|
|
180
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
181
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
182
|
+
});
|
|
183
|
+
await page.waitForTimeout(300);
|
|
184
|
+
}
|
|
185
|
+
expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=analytics-e2e.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-e2e.js","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared analytics journeys for a Brandlab production build. Google is\n * never contacted: the tag loader is stubbed and every dataLayer command is\n * recorded in the test process, across navigations.\n *\n * // tests/e2e/analytics.spec.ts\n * import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'\n *\n * defineBrandlabAnalyticsTests({\n * paths: ['/', '/services/blocked-drains'],\n * contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },\n * })\n *\n * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)\n * so BrandlabGoogleTag renders.\n */\nimport { expect, test, type Page } from '@playwright/test';\nimport { fillBrandlabContactForm, successPattern, type BrandlabContactJourney } from './contact-e2e.js';\nimport {\n ACCEPTED_CONTACT_RESULT,\n RETRYABLE_CONTACT_FAILURE,\n contactEndpointMatcher,\n isolateFromNetwork,\n} from './e2e-support.js';\n\nexport interface BrandlabAnalyticsTestOptions {\n /** Pages to load; the first renders a telephone link. Defaults to ['/']. */\n paths?: readonly string[];\n /** Selector of the telephone link to tap; defaults to the first visible tel: link. */\n phoneLink?: string;\n /** The contact journey whose accepted result emits one contact_form. */\n contact?: BrandlabContactJourney & { retry?: string | RegExp };\n /** Event names the project's analytics contract declares in additionalEvents. */\n additionalEvents?: readonly string[];\n}\n\n/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */\nexport const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([\n 'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',\n]);\n\n/**\n * The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,\n * CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The\n * Google Ads `conversion` event is also allowed, only with an AW- destination.\n */\nexport const BRANDLAB_ANALYTICS_EVENTS = Object.freeze([\n 'contact_form', 'phone_link_click', 'lead_form_start', 'lead_form_error', 'lead_cta_click',\n]);\n\nconst PII_PARAMETER = /^(?:e_?mail|email_address|user_email|contact_email|phone|phone_number|mobile|mobile_number|user_phone|name|first_name|last_name|full_name|customer_name|contact_name|message|enquiry_message|form_message|address|street_address|residential_address|attachment|attachment_name|file_name|filename)$/i;\nconst PHONE_DEDUPE_WINDOW_MS = 1_200;\n\nexport type AnalyticsCommand = unknown[];\n\n/** Record every gtag/dataLayer command the page pushes, surviving navigations. */\nexport async function recordAnalyticsCommands(page: Page): Promise<AnalyticsCommand[]> {\n const commands: AnalyticsCommand[] = [];\n await page.exposeFunction('__brandlabRecordAnalytics', (command: AnalyticsCommand) => { commands.push(command); });\n await page.addInitScript(() => {\n const record = (entry: unknown) => {\n try {\n const command = entry && typeof entry === 'object' && 'length' in (entry as object)\n ? Array.from(entry as ArrayLike<unknown>)\n : [entry];\n void (window as unknown as { __brandlabRecordAnalytics: (value: unknown) => Promise<void> })\n .__brandlabRecordAnalytics(JSON.parse(JSON.stringify(command)));\n } catch { /* an unserialisable command is not analytics evidence */ }\n };\n const wrapped = new WeakSet<unknown[]>();\n const wrap = (layer: unknown[]) => {\n if (wrapped.has(layer)) return layer;\n wrapped.add(layer);\n const push = layer.push.bind(layer);\n layer.push = (...entries: unknown[]) => { entries.forEach(record); return push(...entries); };\n layer.forEach(record);\n return layer;\n };\n let dataLayer = wrap([]);\n Object.defineProperty(window, 'dataLayer', {\n configurable: true,\n get: () => dataLayer,\n // `window.dataLayer = window.dataLayer || []` reassigns the same array.\n set: (value: unknown[]) => { if (Array.isArray(value)) dataLayer = wrap(value); },\n });\n });\n return commands;\n}\n\nexport function analyticsEvents(commands: readonly AnalyticsCommand[], name?: string) {\n return commands\n .filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))\n .map((command) => ({ name: command[1] as string, params: (command[2] ?? {}) as Record<string, unknown> }));\n}\n\n/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */\nexport function analyticsEventProblems(\n commands: readonly AnalyticsCommand[],\n options: { enteredValues?: readonly string[]; additionalEvents?: readonly string[] } = {},\n): string[] {\n const problems: string[] = [];\n const allowed = new Set([...BRANDLAB_ANALYTICS_EVENTS, ...(options.additionalEvents ?? [])]);\n for (const event of analyticsEvents(commands)) {\n if (BRANDLAB_RETIRED_ANALYTICS_EVENTS.includes(event.name)) problems.push(`retired event ${event.name}`);\n else if (event.name === 'conversion') {\n if (!String(event.params.send_to ?? '').startsWith('AW-')) problems.push('conversion event without a Google Ads destination');\n } else if (!allowed.has(event.name)) problems.push(`unexpected event ${event.name}`);\n for (const key of Object.keys(event.params)) {\n if (PII_PARAMETER.test(key)) problems.push(`${event.name} carries the ${key} parameter`);\n }\n }\n const serialised = JSON.stringify(commands).toLowerCase();\n for (const value of (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5)) {\n if (serialised.includes(value.toLowerCase())) problems.push(`an analytics command contains the entered value \"${value}\"`);\n }\n return problems;\n}\n\nexport function defineBrandlabAnalyticsTests(options: BrandlabAnalyticsTestOptions = {}) {\n const paths = options.paths?.length ? options.paths : ['/'];\n const additionalEvents = options.additionalEvents ?? [];\n\n test.describe('Brandlab analytics', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n let commands: AnalyticsCommand[] = [];\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n commands = await recordAnalyticsCommands(page);\n });\n\n test('renders exactly one Google tag on every page', async ({ page }) => {\n for (const path of paths) {\n commands.length = 0;\n await page.goto(path, { waitUntil: 'load' });\n const loaders = page.locator('script[src*=\"googletagmanager.com/gtag/js\"]');\n await expect(loaders.first(), `${path}: no Google tag rendered (build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID)`)\n .toBeAttached({ timeout: 15_000 });\n expect(await loaders.count(), `${path}: renders exactly one Google tag loader`).toBe(1);\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: gtag('js') runs once`).toBe(1);\n const destinations = commands.filter((command) => command[0] === 'config').map((command) => String(command[1]));\n expect(new Set(destinations).size, `${path}: a destination is configured twice`).toBe(destinations.length);\n }\n });\n\n test('sends one phone_link_click per tap', async ({ page }) => {\n await page.goto(paths[0]!, { waitUntil: 'load' });\n const link = options.phoneLink ? page.locator(options.phoneLink).first() : page.locator('a[href^=\"tel:\"]:visible').first();\n await expect(link, 'the page renders a telephone link').toBeVisible();\n await expect.poll(() => commands.some((command) => command[0] === 'js')).toBe(true);\n // Keep the page in place; the shared listener still sees the tap.\n await page.evaluate(() => window.addEventListener('click', (event) => {\n if ((event.target as Element | null)?.closest?.('a[href^=\"tel:\"]')) event.preventDefault();\n }, true));\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(1);\n await page.waitForTimeout(PHONE_DEDUPE_WINDOW_MS + 300);\n expect(analyticsEvents(commands, 'phone_link_click'), 'one tap sends one event').toHaveLength(1);\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(2);\n for (const event of analyticsEvents(commands, 'phone_link_click')) {\n expect(event.params.placement, 'phone_link_click carries its placement').toBeTruthy();\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n\n if (options.contact) {\n const journey = options.contact;\n test('sends one contact_form only after an accepted enquiry, without lead details', async ({ page }) => {\n let attempts = 0;\n await page.route(contactEndpointMatcher(journey.endpoint), async (route) => {\n attempts += 1;\n await route.fulfill(attempts === 1\n ? { status: 503, json: RETRYABLE_CONTACT_FAILURE }\n : { status: 200, json: ACCEPTED_CONTACT_RESULT });\n });\n await fillBrandlabContactForm(page, journey);\n const submit = page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: journey.submit, exact: typeof journey.submit === 'string' });\n await submit.click();\n await expect.poll(() => attempts).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form'), 'a failed delivery is not a conversion').toHaveLength(0);\n const retry = journey.retry ?? journey.submit;\n await page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: retry, exact: typeof retry === 'string' }).click();\n await expect(page).toHaveURL(successPattern(journey.successPath));\n await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);\n expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents })).toEqual([]);\n });\n }\n\n test('sends no retired or web_vital events', async ({ page }) => {\n for (const path of paths) {\n await page.goto(path, { waitUntil: 'load' });\n await page.mouse.wheel(0, 1_200);\n await page.waitForTimeout(300);\n // Web-vitals reporters flush when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });\n document.dispatchEvent(new Event('visibilitychange'));\n window.dispatchEvent(new Event('pagehide'));\n });\n await page.waitForTimeout(300);\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n });\n}\n"]}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,OAAO,EAGL,KAAK,uBAAuB,EAE7B,MAAM,cAAc,CAAC;AAEtB,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,KAAK;IACb,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA0BD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA+BhF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,GAAE,KAGH,GACA,MAAM,CAwBR;AA+ED,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAOT"}
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { checkEngineeringStructure } from "./engineering.js";
|
|
6
|
+
import { validateBrandlabReleaseManifest, } from "./release-manifest.js";
|
|
6
7
|
import { evaluateBrandlabProject, shouldFailBrandlabConformance, } from "./project.js";
|
|
7
8
|
const HELP = `Brandlab verification
|
|
8
9
|
|
|
@@ -12,6 +13,7 @@ Usage:
|
|
|
12
13
|
brandlab-verify [--root <path>]
|
|
13
14
|
[--installed-release <version|unadopted>]
|
|
14
15
|
[--mode <local|ci|preflight>] [--engineering] [--json]
|
|
16
|
+
[--release-manifest <standards/releases/<version>/manifest.json>]
|
|
15
17
|
[--packed-release <receipt.json> (preflight only)]
|
|
16
18
|
|
|
17
19
|
Exit status:
|
|
@@ -19,7 +21,10 @@ Exit status:
|
|
|
19
21
|
preflight uses the same checks against an isolated candidate
|
|
20
22
|
|
|
21
23
|
Static verification reports tests, builds, browsers and deployments as not run
|
|
22
|
-
until real evidence is supplied.
|
|
24
|
+
until real evidence is supplied. A static check passes only when one of its
|
|
25
|
+
rules ran; otherwise it is reported as not run.
|
|
26
|
+
--release-manifest binds every Brandlab package's lockfile integrity to the
|
|
27
|
+
immutable release record.
|
|
23
28
|
--engineering adds the complete-migration structural checks. It does not certify
|
|
24
29
|
manual review, unused-code analysis, runtime journeys or provider cutover.
|
|
25
30
|
`;
|
|
@@ -41,6 +46,8 @@ export function parseBrandlabConformanceArgs(argv) {
|
|
|
41
46
|
parsed.engineering = true;
|
|
42
47
|
else if (argument === "--packed-release")
|
|
43
48
|
parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, "--packed-release"));
|
|
49
|
+
else if (argument === "--release-manifest")
|
|
50
|
+
parsed.releaseManifest = resolve(requireValue(argv, ++index, "--release-manifest"));
|
|
44
51
|
else if (argument === "--root") {
|
|
45
52
|
parsed.root = resolve(requireValue(argv, ++index, "--root"));
|
|
46
53
|
}
|
|
@@ -76,10 +83,13 @@ export function runBrandlabConformanceCli(argv, io = {
|
|
|
76
83
|
installedRelease: options.installedRelease,
|
|
77
84
|
mode: options.mode,
|
|
78
85
|
packedReleaseReceipt: options.packedReleaseReceipt,
|
|
86
|
+
...(options.releaseManifest
|
|
87
|
+
? { releaseManifest: readReleaseManifest(options.releaseManifest) }
|
|
88
|
+
: {}),
|
|
79
89
|
});
|
|
80
90
|
const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;
|
|
81
91
|
io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\n`
|
|
82
|
-
: formatResult(result) + (engineering ?
|
|
92
|
+
: formatResult(result) + (engineering ? formatEngineering(engineering) : ''));
|
|
83
93
|
return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;
|
|
84
94
|
}
|
|
85
95
|
catch (error) {
|
|
@@ -87,6 +97,33 @@ export function runBrandlabConformanceCli(argv, io = {
|
|
|
87
97
|
return 2;
|
|
88
98
|
}
|
|
89
99
|
}
|
|
100
|
+
function formatEngineering(engineering) {
|
|
101
|
+
const findings = engineering.findings.map((finding) => {
|
|
102
|
+
const location = `${finding.path}${finding.line ? `:${finding.line}` : ""}`;
|
|
103
|
+
const label = finding.severity === "info" ? `${finding.rule}/info` : finding.rule;
|
|
104
|
+
return `[${label}] ${location}: ${finding.message}\n`;
|
|
105
|
+
});
|
|
106
|
+
return `Engineering structure: ${engineering.status}\n${engineering.coverage}\n${findings.join("")}`;
|
|
107
|
+
}
|
|
108
|
+
function readReleaseManifest(path) {
|
|
109
|
+
let text;
|
|
110
|
+
try {
|
|
111
|
+
text = readFileSync(path, "utf8");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error(`The release manifest ${path} could not be read.`);
|
|
115
|
+
}
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Parser messages quote file content, so only the path is reported.
|
|
122
|
+
throw new Error(`The release manifest ${path} is invalid JSON.`);
|
|
123
|
+
}
|
|
124
|
+
validateBrandlabReleaseManifest(parsed);
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
90
127
|
function formatResult(result) {
|
|
91
128
|
const externalChecks = result.verification.checks.filter((check) => check.status === "not_run");
|
|
92
129
|
const staticStatus = result.verification.summary.blockers > 0
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { checkEngineeringStructure } from \"./engineering.js\";\nimport {\n evaluateBrandlabProject,\n shouldFailBrandlabConformance,\n type BrandlabConformanceMode,\n type BrandlabConformanceResult,\n} from \"./project.js\";\n\ninterface CliOptions {\n root: string;\n installedRelease?: string | null;\n mode: BrandlabConformanceMode;\n json: boolean;\n help: boolean;\n engineering: boolean;\n packedReleaseReceipt?: string;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nconst HELP = `Brandlab verification\n\nRuns the current objective verification checks for one project.\n\nUsage:\n brandlab-verify [--root <path>]\n [--installed-release <version|unadopted>]\n [--mode <local|ci|preflight>] [--engineering] [--json]\n [--packed-release <receipt.json> (preflight only)]\n\nExit status:\n local/ci fail only for objective current-release blockers\n preflight uses the same checks against an isolated candidate\n\nStatic verification reports tests, builds, browsers and deployments as not run\nuntil real evidence is supplied.\n--engineering adds the complete-migration structural checks. It does not certify\nmanual review, unused-code analysis, runtime journeys or provider cutover.\n`;\n\nexport function parseBrandlabConformanceArgs(argv: readonly string[]): CliOptions {\n const parsed: CliOptions = {\n root: process.cwd(),\n mode: \"local\",\n json: false,\n help: false,\n engineering: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index];\n if (argument === \"--help\" || argument === \"-h\") parsed.help = true;\n else if (argument === \"--json\") parsed.json = true;\n else if (argument === \"--engineering\") parsed.engineering = true;\n else if (argument === \"--packed-release\") parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, \"--packed-release\"));\n else if (argument === \"--root\") {\n parsed.root = resolve(requireValue(argv, ++index, \"--root\"));\n } else if (argument === \"--installed-release\") {\n const value = requireValue(argv, ++index, \"--installed-release\");\n parsed.installedRelease = value === \"unadopted\" ? null : value;\n } else if (argument === \"--mode\") {\n const value = requireValue(argv, ++index, \"--mode\");\n if (![\"local\", \"ci\", \"preflight\"].includes(value)) {\n throw new Error(`Invalid --mode ${value}.`);\n }\n parsed.mode = value as BrandlabConformanceMode;\n } else {\n throw new Error(`Unknown argument ${argument}.`);\n }\n }\n return parsed;\n}\n\nexport function runBrandlabConformanceCli(\n argv: readonly string[],\n io: CliIo = {\n stdout: (value) => process.stdout.write(value),\n stderr: (value) => process.stderr.write(value),\n },\n): number {\n try {\n const options = parseBrandlabConformanceArgs(argv);\n if (options.help) {\n io.stdout(HELP);\n return 0;\n }\n const result = evaluateBrandlabProject({\n root: options.root,\n installedRelease: options.installedRelease,\n mode: options.mode,\n packedReleaseReceipt: options.packedReleaseReceipt,\n });\n const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;\n io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\\n`\n : formatResult(result) + (engineering ? `Engineering structure: ${engineering.status}\\n${engineering.coverage}\\n${engineering.findings.map(f => `[${f.rule}] ${f.path}: ${f.message}\\n`).join('')}` : ''));\n return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n\\n${HELP}`);\n return 2;\n }\n}\n\nfunction formatResult(result: BrandlabConformanceResult): string {\n const externalChecks = result.verification.checks.filter(\n (check) => check.status === \"not_run\",\n );\n const staticStatus =\n result.verification.summary.blockers > 0\n ? \"blocked\"\n : result.verification.summary.warnings > 0\n ? \"warning\"\n : \"pass\";\n const lines = [\n \"Brandlab verification\",\n `installed: ${result.installedRelease ?? \"unadopted\"}`,\n ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),\n `policy: ${result.verification.policyVersion}`,\n `static checks: ${staticStatus}`,\n `external evidence: ${\n externalChecks.length > 0\n ? `not run (${externalChecks.map((check) => check.title).join(\", \")})`\n : \"complete\"\n }`,\n `findings: ${formatSummary(result.verification.summary)}`,\n ];\n for (const family of result.verification.families) {\n if (family.status === \"pass\") continue;\n lines.push(\n `[${family.status}] ${family.title}: ${family.blockers} blocker(s), ${family.warnings} warning(s)`,\n );\n }\n for (const finding of result.operationalFindings) {\n const location = finding.path\n ? ` (${finding.path}${finding.line ? `:${finding.line}` : \"\"})`\n : \"\";\n lines.push(\n `[${finding.familyId}/${finding.severity}] ${finding.ruleId}${location}: ${finding.actual}`,\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction formatSummary(summary: BrandlabConformanceResult[\"summary\"]): string {\n return `${summary.status}; ${summary.blockers} blocker(s), ${summary.warnings} warning(s), ${summary.info} info`;\n}\n\nfunction requireValue(argv: readonly string[], index: number, argument: string): string {\n const value = argv[index];\n if (!value || value.startsWith(\"--\")) throw new Error(`${argument} requires a value.`);\n return value;\n}\n\nexport function isBrandlabConformanceCliEntrypoint(\n moduleUrl: string,\n argvPath: string | undefined,\n): boolean {\n if (!argvPath) return false;\n try {\n return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));\n } catch {\n return false;\n }\n}\n\nif (isBrandlabConformanceCliEntrypoint(import.meta.url, process.argv[1])) {\n process.exitCode = runBrandlabConformanceCli(process.argv.slice(2));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { checkEngineeringStructure, type EngineeringCheck } from \"./engineering.js\";\nimport {\n type BrandlabReleaseManifest,\n validateBrandlabReleaseManifest,\n} from \"./release-manifest.js\";\nimport {\n evaluateBrandlabProject,\n shouldFailBrandlabConformance,\n type BrandlabConformanceMode,\n type BrandlabConformanceResult,\n} from \"./project.js\";\n\ninterface CliOptions {\n root: string;\n installedRelease?: string | null;\n mode: BrandlabConformanceMode;\n json: boolean;\n help: boolean;\n engineering: boolean;\n packedReleaseReceipt?: string;\n releaseManifest?: string;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nconst HELP = `Brandlab verification\n\nRuns the current objective verification checks for one project.\n\nUsage:\n brandlab-verify [--root <path>]\n [--installed-release <version|unadopted>]\n [--mode <local|ci|preflight>] [--engineering] [--json]\n [--release-manifest <standards/releases/<version>/manifest.json>]\n [--packed-release <receipt.json> (preflight only)]\n\nExit status:\n local/ci fail only for objective current-release blockers\n preflight uses the same checks against an isolated candidate\n\nStatic verification reports tests, builds, browsers and deployments as not run\nuntil real evidence is supplied. A static check passes only when one of its\nrules ran; otherwise it is reported as not run.\n--release-manifest binds every Brandlab package's lockfile integrity to the\nimmutable release record.\n--engineering adds the complete-migration structural checks. It does not certify\nmanual review, unused-code analysis, runtime journeys or provider cutover.\n`;\n\nexport function parseBrandlabConformanceArgs(argv: readonly string[]): CliOptions {\n const parsed: CliOptions = {\n root: process.cwd(),\n mode: \"local\",\n json: false,\n help: false,\n engineering: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index];\n if (argument === \"--help\" || argument === \"-h\") parsed.help = true;\n else if (argument === \"--json\") parsed.json = true;\n else if (argument === \"--engineering\") parsed.engineering = true;\n else if (argument === \"--packed-release\") parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, \"--packed-release\"));\n else if (argument === \"--release-manifest\") parsed.releaseManifest = resolve(requireValue(argv, ++index, \"--release-manifest\"));\n else if (argument === \"--root\") {\n parsed.root = resolve(requireValue(argv, ++index, \"--root\"));\n } else if (argument === \"--installed-release\") {\n const value = requireValue(argv, ++index, \"--installed-release\");\n parsed.installedRelease = value === \"unadopted\" ? null : value;\n } else if (argument === \"--mode\") {\n const value = requireValue(argv, ++index, \"--mode\");\n if (![\"local\", \"ci\", \"preflight\"].includes(value)) {\n throw new Error(`Invalid --mode ${value}.`);\n }\n parsed.mode = value as BrandlabConformanceMode;\n } else {\n throw new Error(`Unknown argument ${argument}.`);\n }\n }\n return parsed;\n}\n\nexport function runBrandlabConformanceCli(\n argv: readonly string[],\n io: CliIo = {\n stdout: (value) => process.stdout.write(value),\n stderr: (value) => process.stderr.write(value),\n },\n): number {\n try {\n const options = parseBrandlabConformanceArgs(argv);\n if (options.help) {\n io.stdout(HELP);\n return 0;\n }\n const result = evaluateBrandlabProject({\n root: options.root,\n installedRelease: options.installedRelease,\n mode: options.mode,\n packedReleaseReceipt: options.packedReleaseReceipt,\n ...(options.releaseManifest\n ? { releaseManifest: readReleaseManifest(options.releaseManifest) }\n : {}),\n });\n const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;\n io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\\n`\n : formatResult(result) + (engineering ? formatEngineering(engineering) : ''));\n return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n\\n${HELP}`);\n return 2;\n }\n}\n\nfunction formatEngineering(engineering: EngineeringCheck): string {\n const findings = engineering.findings.map((finding) => {\n const location = `${finding.path}${finding.line ? `:${finding.line}` : \"\"}`;\n const label = finding.severity === \"info\" ? `${finding.rule}/info` : finding.rule;\n return `[${label}] ${location}: ${finding.message}\\n`;\n });\n return `Engineering structure: ${engineering.status}\\n${engineering.coverage}\\n${findings.join(\"\")}`;\n}\n\nfunction readReleaseManifest(path: string): BrandlabReleaseManifest {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n throw new Error(`The release manifest ${path} could not be read.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n // Parser messages quote file content, so only the path is reported.\n throw new Error(`The release manifest ${path} is invalid JSON.`);\n }\n validateBrandlabReleaseManifest(parsed);\n return parsed;\n}\n\nfunction formatResult(result: BrandlabConformanceResult): string {\n const externalChecks = result.verification.checks.filter(\n (check) => check.status === \"not_run\",\n );\n const staticStatus =\n result.verification.summary.blockers > 0\n ? \"blocked\"\n : result.verification.summary.warnings > 0\n ? \"warning\"\n : \"pass\";\n const lines = [\n \"Brandlab verification\",\n `installed: ${result.installedRelease ?? \"unadopted\"}`,\n ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),\n `policy: ${result.verification.policyVersion}`,\n `static checks: ${staticStatus}`,\n `external evidence: ${\n externalChecks.length > 0\n ? `not run (${externalChecks.map((check) => check.title).join(\", \")})`\n : \"complete\"\n }`,\n `findings: ${formatSummary(result.verification.summary)}`,\n ];\n for (const family of result.verification.families) {\n if (family.status === \"pass\") continue;\n lines.push(\n `[${family.status}] ${family.title}: ${family.blockers} blocker(s), ${family.warnings} warning(s)`,\n );\n }\n for (const finding of result.operationalFindings) {\n const location = finding.path\n ? ` (${finding.path}${finding.line ? `:${finding.line}` : \"\"})`\n : \"\";\n lines.push(\n `[${finding.familyId}/${finding.severity}] ${finding.ruleId}${location}: ${finding.actual}`,\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction formatSummary(summary: BrandlabConformanceResult[\"summary\"]): string {\n return `${summary.status}; ${summary.blockers} blocker(s), ${summary.warnings} warning(s), ${summary.info} info`;\n}\n\nfunction requireValue(argv: readonly string[], index: number, argument: string): string {\n const value = argv[index];\n if (!value || value.startsWith(\"--\")) throw new Error(`${argument} requires a value.`);\n return value;\n}\n\nexport function isBrandlabConformanceCliEntrypoint(\n moduleUrl: string,\n argvPath: string | undefined,\n): boolean {\n if (!argvPath) return false;\n try {\n return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));\n } catch {\n return false;\n }\n}\n\nif (isBrandlabConformanceCliEntrypoint(import.meta.url, process.argv[1])) {\n process.exitCode = runBrandlabConformanceCli(process.argv.slice(2));\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Page, type Request } from '@playwright/test';
|
|
2
|
+
export interface BrandlabContactPhotoOptions {
|
|
3
|
+
/** A real image to prepare; a small generated PNG is used when omitted. */
|
|
4
|
+
file?: string;
|
|
5
|
+
/** Accessible name of each prepared photo's remove button. */
|
|
6
|
+
removeButton?: RegExp;
|
|
7
|
+
/** Message shown when a photo cannot be prepared. */
|
|
8
|
+
invalidMessage?: RegExp;
|
|
9
|
+
}
|
|
10
|
+
export interface BrandlabContactJourney {
|
|
11
|
+
/** The page that renders the form, for example '/contact'. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Values to enter, keyed by the contract field key (the control's name). */
|
|
14
|
+
fields: Readonly<Record<string, string>>;
|
|
15
|
+
/** Accessible name of the submit button. */
|
|
16
|
+
submit: string | RegExp;
|
|
17
|
+
/** Where an accepted enquiry lands; defaults to /thank-you. */
|
|
18
|
+
successPath?: string | RegExp;
|
|
19
|
+
/** Selector of the form; defaults to the form containing the first field. */
|
|
20
|
+
form?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Pathname of the mocked contact endpoint; defaults to the shared /api/contact.
|
|
23
|
+
* Matched by pathname, so a trailing slash or query string is included.
|
|
24
|
+
*/
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface BrandlabContactTestOptions extends BrandlabContactJourney {
|
|
28
|
+
/** Accessible name of the retry control after a retryable failure; defaults to submit. */
|
|
29
|
+
retry?: string | RegExp;
|
|
30
|
+
/** Enable the photo journeys when the form accepts attachments. */
|
|
31
|
+
photos?: boolean | BrandlabContactPhotoOptions;
|
|
32
|
+
}
|
|
33
|
+
export declare function defineBrandlabContactTests(options: BrandlabContactTestOptions): void;
|
|
34
|
+
/**
|
|
35
|
+
* Open the contact page, wait for hydration and enter every field value. The page
|
|
36
|
+
* is isolated first, so submitting without a mock aborts the request rather than
|
|
37
|
+
* delivering an enquiry.
|
|
38
|
+
*/
|
|
39
|
+
export declare function fillBrandlabContactForm(page: Page, journey: BrandlabContactJourney): Promise<void>;
|
|
40
|
+
export declare function successPattern(successPath?: string | RegExp): RegExp;
|
|
41
|
+
/** Wait for mocked contact requests, naming any submission the guard blocked instead. */
|
|
42
|
+
export declare function expectContactRequests(page: Page, requests: readonly Request[], count: number): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=contact-e2e.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact-e2e.d.ts","sourceRoot":"","sources":["../src/contact-e2e.ts"],"names":[],"mappings":"AAiBA,OAAO,EAA8B,KAAK,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAYvF,MAAM,WAAW,2BAA2B;IAC1C,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,4CAA4C;IAC5C,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA2B,SAAQ,sBAAsB;IACxE,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,GAAG,2BAA2B,CAAC;CAChD;AAID,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,0BAA0B,QA4G7E;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,sBAAsB,iBAcxF;AAED,wBAAgB,cAAc,CAAC,WAAW,GAAE,MAAM,GAAG,MAAqB,GAAG,MAAM,CAElF;AAED,yFAAyF;AACzF,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,iBASlG"}
|