diff --git a/scripts/format-scope.json b/scripts/format-scope.json
index ea5240d82..16dbe251a 100644
--- a/scripts/format-scope.json
+++ b/scripts/format-scope.json
@@ -156,5 +156,6 @@
"scripts/qa-map-source-controls.mjs",
"src/ui/layers.js",
"src/ui/layerPanel.js",
- "src/ui/clearLayersControl.js"
+ "src/ui/clearLayersControl.js",
+ "scripts/qa-layer-panel.mjs"
]
diff --git a/scripts/qa-layer-panel.mjs b/scripts/qa-layer-panel.mjs
new file mode 100644
index 000000000..ef458bee7
--- /dev/null
+++ b/scripts/qa-layer-panel.mjs
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+/** Browser proof of Layers panel replacement and subscription ownership. */
+import puppeteer from 'puppeteer';
+const browser = await puppeteer.launch({
+ headless: true,
+ args: [
+ '--no-sandbox',
+ ...(process.platform === 'darwin'
+ ? ['--use-angle=metal', '--enable-gpu']
+ : ['--use-gl=angle', '--use-angle=swiftshader']),
+ ],
+});
+const page = await browser.newPage();
+let failures = 0;
+const errors = [];
+page.on('pageerror', (error) => errors.push(error.message));
+const check = (name, passed) => {
+ console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
+ if (!passed) failures++;
+};
+try {
+ await page.goto(
+ `${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
+ { waitUntil: 'domcontentloaded' },
+ );
+ await page.waitForFunction(
+ () => window.__godsEyeView?.dataManager?._layerPanel,
+ { timeout: 60000 },
+ );
+ const results = await page.evaluate(async () => {
+ const manager = window.__godsEyeView.dataManager;
+ const container = manager._toggleContainer;
+ const id = 'qa-panel-lifecycle';
+ let listener = null;
+ let enabled = 0;
+ window.__gevQaRegisterLayer(manager, {
+ id,
+ name: 'Literal layer',
+ icon: '◌',
+ source: 'Local fixture',
+ updateInterval: -1,
+ init() {},
+ enable() {
+ enabled++;
+ },
+ disable() {},
+ update() {},
+ destroy() {},
+ getStats: () => ({ count: 1250, lastUpdate: Date.now(), stale: true }),
+ getRowControls: () => ({ chips: [] }),
+ setRowControlsListener: (callback) => {
+ listener = callback;
+ },
+ });
+ const result = [];
+ try {
+ manager.buildTogglePanel(container);
+ const row = () => container.querySelector(`[data-layer-id="${id}"]`);
+ const first = row().querySelector('.data-toggle-btn');
+ result.push([
+ 'descriptor text is literal content',
+ row().querySelector('.data-name').textContent ===
+ 'Literal layer' && !row().querySelector('b'),
+ ]);
+ result.push([
+ 'count is presented from the supplied snapshot',
+ row().querySelector('.data-count').textContent === '1.3K',
+ ]);
+ result.push([
+ 'row subscription is installed',
+ typeof listener === 'function',
+ ]);
+ manager.buildTogglePanel(container);
+ first.click();
+ await Promise.resolve();
+ result.push([
+ 'a detached row cannot issue an enable action',
+ enabled === 0 && !manager.isEnabled(id),
+ ]);
+ await manager.setEnabled(id, true);
+ result.push([
+ 'feed state reflects the settled layer snapshot',
+ row().querySelector('.data-toggle-btn').dataset.feedState === 'stale',
+ ]);
+ const final = row().querySelector('.data-toggle-btn');
+ manager._layerPanel.destroy();
+ final.click();
+ await Promise.resolve();
+ result.push([
+ 'destroyed panel releases row subscriptions',
+ listener === null,
+ ]);
+ result.push([
+ 'destroyed controls cannot change the layer',
+ manager.isEnabled(id),
+ ]);
+ } finally {
+ manager._layerPanel?.destroy();
+ manager._layerPanel = null;
+ await window.__gevQaUnregisterLayer(manager, id);
+ manager.buildTogglePanel(container);
+ }
+ result.push([
+ 'ordinary layer rows return after reassembly',
+ container.querySelectorAll('.data-toggle-row').length > 10,
+ ]);
+ return result;
+ });
+ for (const [name, passed] of results) check(name, passed);
+ check('no uncaught browser errors', errors.length === 0);
+} finally {
+ await browser.close();
+}
+console.log(`RESULT: ${failures} failures`);
+process.exitCode = failures ? 1 : 0;