Skip to content

Commit 5507218

Browse files
committed
Upgrading to 3.0.0
1 parent 719b001 commit 5507218

8 files changed

Lines changed: 115 additions & 3 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ dmypy.json
5757
/test/*
5858
/tests/
5959
/scripts/*
60+
!/scripts/audit_frontend_release.mjs
61+
!/scripts/audit_frontend_ownership.mjs
62+
!/scripts/audit_frontend_scoped_wiring.mjs
63+
!/scripts/audit_frontend_pseudo_locale.mjs
64+
!/scripts/audit_frontend_degraded_states.mjs
6065
/custom_components/blueprint_studio/www/component-showcase.html
6166
/custom_components/blueprint_studio/www/modules/component-showcase.js
6267
/custom_components/blueprint_studio/www/styles/component-showcase.css

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[pytest]
22
asyncio_mode = auto
3+
pythonpath = .
34
testpaths = tests
45
addopts = -ra
56
markers =
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
const root = new URL('../custom_components/blueprint_studio/www/modules/', import.meta.url);
4+
const contracts = [
5+
['global-search.js', 'AbortController', 'stale search cancellation'],
6+
['downloads-uploads.js', 'AbortController', 'transfer cancellation'],
7+
['api.js', 'Session expired. Please login again.', 'expired Home Assistant authentication'],
8+
['github-integration.js', 'GitHub authentication is no longer valid', 'expired provider authentication'],
9+
['activity-rail.js', "'unavailable'", 'unavailable feature state'],
10+
['problems.js', "validation:stale", 'stale validation results'],
11+
['ha-autocomplete.js', "type: 'stale'", 'stale Home Assistant metadata'],
12+
];
13+
for (const [file, marker, description] of contracts) {
14+
const source = await readFile(new URL(file, root), 'utf8');
15+
if (!source.includes(marker)) throw new Error(`${description} contract missing in ${file}`);
16+
console.log(`PASS ${description}`);
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
const app = await readFile(new URL('../custom_components/blueprint_studio/www/modules/app.js', import.meta.url), 'utf8');
4+
const localDefinitions = app.match(/^(?:export\s+)?(?:async\s+)?function\s+\w+/gm) || [];
5+
const hasCompatibilityExport = app.includes('export {') && app.includes('initializeEventHandlers');
6+
console.log(`app.js local feature definitions: ${localDefinitions.length}`);
7+
console.log(`app.js compatibility export surface: ${hasCompatibilityExport ? 'present' : 'missing'}`);
8+
if (localDefinitions.length || !hasCompatibilityExport) throw new Error('app.js must remain a compatibility export surface, not a second feature implementation');
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
const source = await readFile(new URL('../custom_components/blueprint_studio/www/modules/translations.js', import.meta.url), 'utf8');
4+
const english = JSON.parse(await readFile(new URL('../custom_components/blueprint_studio/www/locales/en.json', import.meta.url), 'utf8'));
5+
if (!source.includes("currentLang === 'en-XA'")) throw new Error('en-XA pseudo-locale is not wired into translation initialization');
6+
if (!source.includes('createPseudoBundle') || !source.includes("[[ ${expanded} ]]")) throw new Error('pseudo-locale expansion contract is missing');
7+
8+
const samples = Object.entries(english).filter(([, value]) => typeof value === 'string' && value.length >= 12).slice(0, 25);
9+
const expanded = value => `[[ ${String(value).replace(/\{[^}]+\}|[^\s]+/g, part => part.startsWith('{') ? part : `${part}${'~'.repeat(Math.max(1, Math.ceil(part.length * 0.35)))}`)} ]]`;
10+
for (const [key, value] of samples) {
11+
const pseudo = expanded(value);
12+
const placeholders = value.match(/\{[^}]+\}/g) || [];
13+
if (pseudo.length < value.length * 1.2) throw new Error(`pseudo string is not expanded enough: ${key}`);
14+
for (const placeholder of placeholders) if (!pseudo.includes(placeholder)) throw new Error(`placeholder lost for ${key}: ${placeholder}`);
15+
}
16+
console.log(`PASS pseudo-locale expansion (${samples.length} representative strings, >=20% growth)`);

scripts/audit_frontend_release.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { readFile } from 'node:fs/promises';
3+
4+
const live = process.argv.includes('--live');
5+
const checks = [
6+
['ownership', 'scripts/audit_frontend_ownership.mjs'],
7+
['scoped wiring', 'scripts/audit_frontend_scoped_wiring.mjs'],
8+
];
9+
const requiredArtifacts = [
10+
'custom_components/blueprint_studio/www/panels/panel_custom.html',
11+
'custom_components/blueprint_studio/www/modules/app.js',
12+
'custom_components/blueprint_studio/www/modules/translations.js',
13+
'custom_components/blueprint_studio/www/locales/en.json',
14+
'scripts/audit_frontend_ownership.mjs',
15+
'scripts/audit_frontend_scoped_wiring.mjs',
16+
'scripts/audit_frontend_pseudo_locale.mjs',
17+
'scripts/audit_frontend_degraded_states.mjs',
18+
];
19+
20+
const run = (label, command, args = []) => {
21+
const result = spawnSync(command, args, { stdio: 'inherit', env: process.env });
22+
if (result.status !== 0) throw new Error(`${label} failed`);
23+
};
24+
25+
for (const [label, script] of checks) run(label, 'node', [script]);
26+
run('pseudo locale', 'node', ['scripts/audit_frontend_pseudo_locale.mjs']);
27+
run('degraded states', 'node', ['scripts/audit_frontend_degraded_states.mjs']);
28+
for (const artifact of requiredArtifacts) await readFile(new URL(`../${artifact}`, import.meta.url));
29+
console.log(`PASS release artifact inventory (${requiredArtifacts.length} files)`);
30+
31+
if (live) {
32+
const liveChecks = [
33+
['performance budgets', 'scripts/audit_frontend_performance_budgets.mjs'],
34+
['lazy dependencies', 'scripts/audit_frontend_lazy_dependencies.mjs'],
35+
['large workspace', 'scripts/audit_frontend_large_workspace.mjs'],
36+
['reinitialization', 'scripts/audit_frontend_reinitialization.mjs'],
37+
['viewport screenshots', 'scripts/audit_frontend_screenshots.mjs'],
38+
['shortcut guide', 'scripts/audit_frontend_shortcuts.mjs'],
39+
['Playwright layouts', 'scripts/audit_frontend_playwright.mjs'],
40+
];
41+
for (const [label, script] of liveChecks) run(label, 'node', [script]);
42+
}
43+
44+
console.log(live ? 'Frontend release gate passed (static + live)' : 'Frontend release gate passed (CI static)');
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { readFile } from 'node:fs/promises';
2+
3+
const root = new URL('../custom_components/blueprint_studio/www/modules/', import.meta.url);
4+
const checks = [
5+
['ai-ui.js', 'document.querySelectorAll(\'[data-ai-mode]\')', 'AI mode controls are scoped to #ai-sidebar'],
6+
['split-view.js', 'document.querySelectorAll(\'.tab.drop-target', 'Split drag state is scoped to tab containers'],
7+
['translations.js', 'document.querySelectorAll(".search-mode-tab")', 'Search tabs are scoped to #view-search'],
8+
['ui.js', 'document.querySelectorAll(".theme-menu-item")', 'Theme items are scoped to the theme menu'],
9+
['coordinators/UICoordinator.js', 'document.querySelectorAll(".tree-item.active")', 'Tree active state is scoped to #file-tree'],
10+
['git-diff.js', 'document.querySelectorAll(".git-history-item")', 'Git history is scoped to the active modal'],
11+
];
12+
const failures = [];
13+
for (const [file, forbidden, explanation] of checks) {
14+
const source = await readFile(new URL(file, root), 'utf8');
15+
if (source.includes(forbidden)) failures.push(`${file}: ${explanation}`);
16+
else console.log(`PASS ${file}: ${explanation}`);
17+
}
18+
if (failures.length) {
19+
for (const failure of failures) console.error(`FAIL ${failure}`);
20+
process.exitCode = 1;
21+
}

tests/test_frontend_contract.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5025,11 +5025,11 @@ def test_app_ownership_audit_keeps_compatibility_surface_thin(self):
50255025

50265026
def test_frontend_release_gate_covers_ci_and_live_artifacts(self):
50275027
gate = (ROOT / "scripts" / "audit_frontend_release.mjs").read_text(encoding="utf-8")
5028-
quality = (ROOT / "FRONTEND_QUALITY_GATES.md").read_text(encoding="utf-8")
50295028
workflow = (ROOT / ".github" / "workflows" / "frontend-quality.yaml").read_text(encoding="utf-8")
50305029
self.assertIn("--live", gate)
5031-
self.assertIn("FRONTEND_QUALITY_GATES.md", gate)
5032-
self.assertIn("pytest -q", quality)
5030+
self.assertIn("custom_components/blueprint_studio/www/panels/panel_custom.html", gate)
5031+
self.assertIn("scripts/audit_frontend_pseudo_locale.mjs", gate)
5032+
self.assertNotIn("FRONTEND_QUALITY_GATES.md", gate)
50335033
self.assertIn("audit_frontend_release.mjs", workflow)
50345034

50355035
def test_frontend_test_matrix_names_primary_workflows_and_limits(self):

0 commit comments

Comments
 (0)