Skip to content

Commit d63e9c9

Browse files
author
FolderView Plus Test
committed
Extract Docker reconcile runtime and add browser update-flow smoke
1 parent fe7d598 commit d63e9c9

12 files changed

Lines changed: 512 additions & 183 deletions

archive/folderview.plus-2026.04.08.08.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5058b41020da88ce2ff6446655d21f2e33cfdeb2aee300d2f4c8529139f65e8d folderview.plus-2026.04.14.12.txz

folderview.plus.plg

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.04.14.11">
10-
<!ENTITY md5 "57d82d7b18a26a8fbedc594d74331df3">
9+
<!ENTITY version "2026.04.14.12">
10+
<!ENTITY md5 "30d83c8cf8ca389091433e0bac0bb10c">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.04.14.12
17+
- Fix: Docker support-bundle snapshots, trace storage caps, and rendered-state diagnostics.
18+
- Fix: Docker runtime rows, folder state, and container interactions.
19+
- Refactor: Shared runtime contracts, request plumbing, and cross-page foundations.
20+
- Quality: Release automation, CI smoke coverage, and packaging guards.
21+
22+
1623
###2026.04.14.11
1724
- Fix: Docker support-bundle snapshots, trace storage caps, and rendered-state diagnostics.
1825
- Fix: Docker runtime rows, folder state, and container interactions.

scripts/browser_smoke.mjs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const runtimeReports = [];
6060
const dashboardReports = [];
6161
const folderEditorReports = [];
6262
const dockerDiagnosticsReports = [];
63+
const dockerUpdateFlowReports = [];
6364

6465
const resolveRuntimeUrl = (baseUrl, type) => {
6566
try {
@@ -570,6 +571,170 @@ const runDockerDiagnosticsSmoke = async (page, { browserName, url }) => {
570571
};
571572
};
572573

574+
const runDockerUpdateFlowSmoke = async (page, { browserName, url }) => {
575+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
576+
await page.waitForTimeout(1200);
577+
578+
const report = await page.evaluate(async () => {
579+
const bulkKey = 'fv.support.bundle.docker.bulkUpdateTrace.v1';
580+
const requestKey = 'fv.support.bundle.docker.requestBundleTrace.v1';
581+
const pageKey = 'fv.support.bundle.docker.page.v1';
582+
const readRecord = (storageKey) => {
583+
try {
584+
const raw = window.localStorage?.getItem(storageKey);
585+
return raw ? JSON.parse(raw) : null;
586+
} catch (_error) {
587+
return null;
588+
}
589+
};
590+
const waitForRecord = async (predicate, timeoutMs = 6000, stepMs = 100) => {
591+
const startedAt = Date.now();
592+
while ((Date.now() - startedAt) < timeoutMs) {
593+
const value = predicate();
594+
if (value) {
595+
return value;
596+
}
597+
await new Promise((resolve) => window.setTimeout(resolve, stepMs));
598+
}
599+
return null;
600+
};
601+
if (typeof window.updateFolder !== 'function') {
602+
return {
603+
skipped: true,
604+
reason: 'window.updateFolder is unavailable on the Docker page.'
605+
};
606+
}
607+
if (typeof window.loadlist !== 'function') {
608+
return {
609+
skipped: true,
610+
reason: 'window.loadlist is unavailable on the Docker page.'
611+
};
612+
}
613+
const updateLink = Array.from(document.querySelectorAll('#docker_list td.updatecolumn a.exec, #docker_view td.updatecolumn a.exec'))
614+
.find((node) => {
615+
const text = String(node?.closest('td.updatecolumn')?.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
616+
return /apply update/.test(text) && /updateFolder\(/.test(String(node?.getAttribute('onclick') || ''));
617+
});
618+
if (!(updateLink instanceof HTMLAnchorElement)) {
619+
return {
620+
skipped: true,
621+
reason: 'No folder apply update row available for synthetic update-flow smoke.'
622+
};
623+
}
624+
const folderRow = updateLink.closest('tr');
625+
const folderIdMatch = String(updateLink.getAttribute('onclick') || '').match(/updateFolder\(\s*['"]?([^,'")\s]+)['"]?/i);
626+
const folderId = String(folderIdMatch?.[1] || '').trim();
627+
if (!folderId) {
628+
return {
629+
skipped: true,
630+
reason: 'Unable to resolve the target folder id for synthetic update-flow smoke.'
631+
};
632+
}
633+
634+
const beforeBulkTrace = readRecord(bulkKey);
635+
const beforeRequestTrace = readRecord(requestKey);
636+
const beforeBulkCount = Number(beforeBulkTrace?.count || 0);
637+
const beforeRequestCount = Number(beforeRequestTrace?.count || 0);
638+
const capturedCommands = [];
639+
const originalOpenDocker = window.openDocker;
640+
641+
try {
642+
window.openDocker = function(...args) {
643+
capturedCommands.push(Array.isArray(args) ? args.map((entry) => String(entry ?? '')) : []);
644+
return undefined;
645+
};
646+
647+
window.updateFolder(folderId);
648+
649+
const bulkTrace = await waitForRecord(() => {
650+
const record = readRecord(bulkKey);
651+
if (!record || Number(record?.count || 0) < beforeBulkCount) {
652+
return null;
653+
}
654+
const entries = Array.isArray(record?.entries) ? record.entries : [];
655+
const nextEntries = entries.slice(beforeBulkCount);
656+
const sawDispatch = nextEntries.some((entry) => entry?.eventType === 'updateFolderDispatch');
657+
const sawDialogOpened = nextEntries.some((entry) => entry?.eventType === 'dialogOpened');
658+
const sawReconcileWindow = nextEntries.some((entry) => entry?.eventType === 'reconcileWindowArmed');
659+
return sawDispatch && sawDialogOpened && sawReconcileWindow ? record : null;
660+
}, 6000);
661+
662+
window.loadlist();
663+
664+
const requestTrace = await waitForRecord(() => {
665+
const record = readRecord(requestKey);
666+
if (!record || Number(record?.count || 0) < beforeRequestCount) {
667+
return null;
668+
}
669+
const entries = Array.isArray(record?.entries) ? record.entries : [];
670+
const nextEntries = entries.slice(beforeRequestCount);
671+
const sawLoadlist = nextEntries.some((entry) => entry?.eventType === 'loadlist');
672+
const matchingBuild = nextEntries.find((entry) =>
673+
entry?.eventType === 'buildDockerFolderReq'
674+
&& entry?.liveUpdateStatus === true
675+
&& entry?.hostSyncSuspended === true
676+
);
677+
const sawListview = nextEntries.some((entry) => entry?.eventType === 'listview');
678+
return sawLoadlist && matchingBuild && sawListview ? { record, matchingBuild } : null;
679+
}, 6000);
680+
681+
const pageSnapshot = await waitForRecord(() => {
682+
const record = readRecord(pageKey);
683+
return record?.currentPage === '/Docker' ? record : null;
684+
}, 3000);
685+
686+
return {
687+
skipped: false,
688+
pass: Boolean(bulkTrace) && Boolean(requestTrace),
689+
folderId,
690+
capturedCommandCount: capturedCommands.length,
691+
capturedFirstCommand: capturedCommands[0]?.[0] || null,
692+
visibleFolderClass: String(folderRow?.className || ''),
693+
bulkTraceCount: Number(bulkTrace?.count || 0),
694+
requestTraceCount: Number(requestTrace?.record?.count || 0),
695+
liveUpdateStatusObserved: requestTrace?.matchingBuild?.liveUpdateStatus === true,
696+
hostSyncSuspendedObserved: requestTrace?.matchingBuild?.hostSyncSuspended === true,
697+
pageSnapshotAvailable: Boolean(pageSnapshot)
698+
};
699+
} finally {
700+
window.openDocker = originalOpenDocker;
701+
}
702+
});
703+
704+
const screenshotName = `${sanitizeToken(scenarioLabel)}-${sanitizeToken(browserName)}-docker-update-flow.png`;
705+
const screenshotPath = path.join(artifactRoot, screenshotName);
706+
await page.screenshot({ path: screenshotPath, fullPage: true });
707+
708+
if (report?.skipped) {
709+
console.warn(`Docker update-flow smoke skipped for ${browserName}: ${report.reason}`);
710+
return {
711+
browserName,
712+
url,
713+
skipped: true,
714+
pass: false,
715+
reason: report.reason,
716+
screenshotPath
717+
};
718+
}
719+
720+
if (!report?.pass) {
721+
throw new Error(
722+
`Docker update-flow smoke failed for ${browserName}: ${JSON.stringify(report)}. `
723+
+ `Screenshot: ${screenshotPath}`
724+
);
725+
}
726+
727+
console.log(`Docker update-flow smoke passed: ${browserName} ${JSON.stringify(report)}`);
728+
return {
729+
browserName,
730+
url,
731+
skipped: false,
732+
pass: true,
733+
screenshotPath,
734+
...report
735+
};
736+
};
737+
573738
const runDashboardQuickRailSmoke = async (page, { browserName, url }) => {
574739
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
575740
await page.waitForTimeout(1200);
@@ -1172,6 +1337,13 @@ const runBrowserSmoke = async (browserName, browserType) => {
11721337
if (diagnosticsReport) {
11731338
dockerDiagnosticsReports.push(diagnosticsReport);
11741339
}
1340+
const updateFlowReport = await runDockerUpdateFlowSmoke(page, {
1341+
browserName,
1342+
url: dockerRuntimeTarget.url
1343+
});
1344+
if (updateFlowReport) {
1345+
dockerUpdateFlowReports.push(updateFlowReport);
1346+
}
11751347
}
11761348

11771349
if (dashboardUrl) {
@@ -1223,6 +1395,7 @@ try {
12231395
scenarioLabel,
12241396
reports: runtimeReports,
12251397
dockerDiagnosticsReports,
1398+
dockerUpdateFlowReports,
12261399
dashboardReports,
12271400
folderEditorReports
12281401
};

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/folderview.plus.Docker.page

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ $fvplusRuntimePreflightHasFatal = runtimePreflightHasFatal($fvplusRuntimePreflig
6666
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.actions.js')?>"></script>
6767
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.host-guards.js')?>"></script>
6868
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.diagnostics.js')?>"></script>
69+
<script src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.runtime.reconcile.js')?>"></script>
6970
<script defer src="<?php fvplus_asset('/plugins/folderview.plus/scripts/docker.js')?>"></script>
7071

7172
<link rel="stylesheet" href="<?php fvplus_asset('/plugins/folderview.plus/styles/runtime.shared.css')?>">

0 commit comments

Comments
 (0)