@wenathlan/devthink 2.0.13 → 2.0.15
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/CHANGELOG.md +27 -0
- package/accounts.ts +2087 -0
- package/agent.ts +2331 -0
- package/antigravity.ts +2139 -0
- package/apifreeze.ts +4146 -0
- package/auth.ts +642 -0
- package/background.ts +16837 -0
- package/bridge.ts +2080 -0
- package/build.ts +54 -0
- package/bun.ts +26 -0
- package/capture.ts +1614 -0
- package/checksecrets.ts +72 -0
- package/cjs.ts +9 -0
- package/cli.ts +3500 -0
- package/commands.ts +1101 -0
- package/companion.ts +450 -0
- package/compatibility.ts +37 -0
- package/config.ts +3137 -0
- package/constants.ts +943 -0
- package/core.ts +1473 -0
- package/crossbrowser.ts +1359 -0
- package/dashdone.ts +605 -0
- package/data.ts +943 -0
- package/database.ts +133 -0
- package/debug.ts +850 -0
- package/deno.ts +27 -0
- package/devthink.ts +707 -0
- package/dist/accounts.d.ts +409 -0
- package/dist/agent.d.ts +837 -0
- package/dist/antigravity.d.ts +380 -0
- package/dist/apifreeze.d.ts +113 -0
- package/dist/auth.d.ts +224 -0
- package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
- package/dist/capture.d.ts +595 -0
- package/dist/cjs.d.ts +6 -0
- package/{cli.js → dist/cli.js} +2 -2
- package/dist/commands.d.ts +310 -0
- package/dist/compatibility.d.ts +17 -0
- package/dist/config.d.ts +1853 -0
- package/dist/constants.d.ts +637 -0
- package/dist/core.d.ts +361 -0
- package/dist/data.d.ts +330 -0
- package/dist/database.d.ts +24 -0
- package/dist/debug.d.ts +270 -0
- package/dist/engine.d.ts +74 -0
- package/dist/environments.d.ts +244 -0
- package/dist/evidence.d.ts +277 -0
- package/dist/export.d.ts +311 -0
- package/dist/fingerprint.d.ts +594 -0
- package/dist/flow.d.ts +300 -0
- package/dist/gates.d.ts +103 -0
- package/dist/identity.d.ts +51 -0
- package/dist/ids.d.ts +7 -0
- package/dist/llm.d.ts +652 -0
- package/dist/models.d.ts +1807 -0
- package/dist/net.d.ts +325 -0
- package/dist/neutral.d.ts +41 -0
- package/dist/oauth.d.ts +703 -0
- package/dist/page.d.ts +1166 -0
- package/dist/perf.d.ts +481 -0
- package/dist/plan.d.ts +134 -0
- package/dist/project.d.ts +395 -0
- package/dist/providers.d.ts +37 -0
- package/dist/quota.d.ts +453 -0
- package/dist/recovery.d.ts +621 -0
- package/dist/request.d.ts +1492 -0
- package/dist/run.d.ts +779 -0
- package/dist/runtime.d.ts +212 -0
- package/dist/search.d.ts +375 -0
- package/dist/security.d.ts +534 -0
- package/dist/serve.d.ts +136 -0
- package/dist/session.d.ts +296 -0
- package/dist/storage.d.ts +53 -0
- package/dist/streaming.d.ts +1119 -0
- package/dist/swarm.d.ts +328 -0
- package/dist/tools.d.ts +170 -0
- package/dist/types.d.ts +8357 -0
- package/dist/utils.d.ts +82 -0
- package/dist/version.d.ts +7 -0
- package/dist/versionregistry.d.ts +406 -0
- package/dist/views.d.ts +579 -0
- package/dist/workbenchsession.d.ts +69 -0
- package/dist/workflow.d.ts +649 -0
- package/docs/readme.md +92 -0
- package/docs/schemas/antigravity.json +192 -0
- package/engine.ts +1524 -0
- package/environments.ts +1005 -0
- package/evidence.ts +643 -0
- package/export.ts +889 -0
- package/fingerprint.ts +1458 -0
- package/flow.ts +1190 -0
- package/gates.ts +307 -0
- package/gateway.ts +34 -0
- package/hardening.ts +191 -0
- package/headless.ts +369 -0
- package/http.ts +2110 -0
- package/identity.ts +246 -0
- package/ids.ts +26 -0
- package/index.ts +369 -0
- package/llm.ts +2682 -0
- package/mcp.ts +1835 -0
- package/{memory.js → memory.ts} +3834 -2310
- package/models.ts +3805 -0
- package/modes.ts +43 -0
- package/net.ts +1214 -0
- package/neutral.ts +362 -0
- package/node.ts +26 -0
- package/oauth.ts +2216 -0
- package/offscreen.ts +170 -0
- package/pack.ts +956 -0
- package/package.json +175 -82
- package/page.ts +7824 -0
- package/pagebridge.ts +1116 -0
- package/perf.ts +957 -0
- package/plan.ts +672 -0
- package/plugin.ts +76 -0
- package/{policy.js → policy.ts} +5073 -4679
- package/prisma.config.ts +27 -0
- package/progress.ts +1308 -0
- package/project.ts +1291 -0
- package/protocol.ts +4187 -0
- package/providers.ts +246 -0
- package/quota.ts +1887 -0
- package/recovery.ts +2433 -0
- package/request.ts +4482 -0
- package/run.ts +2135 -0
- package/runtime.ts +526 -0
- package/search.ts +1760 -0
- package/security.ts +1673 -0
- package/serve.ts +419 -0
- package/server.ts +5538 -0
- package/session.ts +1162 -0
- package/storage.ts +182 -0
- package/streaming.ts +4101 -0
- package/swarm.ts +884 -0
- package/sync.ts +56 -0
- package/tests/agentcert.test.ts +128 -0
- package/tests/agentfleet.test.ts +214 -0
- package/tests/agentreview.test.ts +241 -0
- package/tests/agentstream.test.ts +335 -0
- package/tests/agentwork.test.ts +356 -0
- package/tests/apifreeze.test.ts +290 -0
- package/tests/apimap.test.ts +112 -0
- package/tests/approvalgate.test.ts +175 -0
- package/tests/artifactmanifest.test.ts +204 -0
- package/tests/attentionfeed.test.ts +163 -0
- package/tests/backgroundruns.test.ts +100 -0
- package/tests/batchscheduling.test.ts +131 -0
- package/tests/blackboard.test.ts +238 -0
- package/tests/bridge.test.ts +1088 -0
- package/tests/browsercoverage.test.ts +134 -0
- package/tests/browserpolyfills.test.ts +124 -0
- package/tests/build.mjs +1260 -0
- package/tests/clientauth.test.ts +223 -0
- package/tests/clitools.test.ts +578 -0
- package/tests/confirmgates.test.ts +190 -0
- package/tests/containerpack.test.ts +143 -0
- package/tests/controlflow.test.ts +2120 -0
- package/tests/coordination.test.ts +345 -0
- package/tests/copyscreen.test.ts +139 -0
- package/tests/core.test.ts +417 -0
- package/tests/costcert.test.ts +95 -0
- package/tests/crossbrowser.test.ts +115 -0
- package/tests/cspaudit.test.ts +79 -0
- package/tests/dashdone.test.ts +615 -0
- package/tests/datagrid.test.ts +137 -0
- package/tests/doccheck.test.ts +107 -0
- package/tests/dombatch.test.ts +109 -0
- package/tests/emulation.test.ts +282 -0
- package/tests/environments.test.ts +259 -0
- package/tests/evidenceviews.test.ts +88 -0
- package/tests/exporttools.test.ts +185 -0
- package/tests/firefoxprep.test.ts +134 -0
- package/tests/flowlibrary.test.ts +242 -0
- package/tests/flowrun.test.ts +303 -0
- package/tests/forensics.test.ts +439 -0
- package/tests/gateway.test.ts +939 -0
- package/tests/headlesslib.test.ts +171 -0
- package/tests/httpstream.test.ts +277 -0
- package/tests/immutablelog.test.ts +157 -0
- package/tests/inboundguard.test.ts +177 -0
- package/tests/lazyload.test.ts +72 -0
- package/tests/librarymodes.test.ts +623 -0
- package/tests/llm.test.ts +696 -0
- package/tests/manifest.test.ts +226 -0
- package/tests/maskinputs.test.ts +150 -0
- package/tests/matrixverify.test.ts +111 -0
- package/tests/mavenpack.test.ts +75 -0
- package/tests/mcpserver.test.ts +704 -0
- package/tests/memory.test.ts +4155 -0
- package/tests/memorycare.test.ts +230 -0
- package/tests/migrateplan.test.ts +349 -0
- package/tests/minimization.test.ts +414 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/multiagent.test.ts +440 -0
- package/tests/nativebridge.test.ts +807 -0
- package/tests/navigation.test.ts +284 -0
- package/tests/nugetpack.test.ts +83 -0
- package/tests/orchestration.test.ts +449 -0
- package/tests/originpolicy.test.ts +405 -0
- package/tests/outputcompare.test.ts +97 -0
- package/tests/package.test.ts +91 -0
- package/tests/pagebridge.test.ts +5909 -0
- package/tests/pentest.test.ts +75 -0
- package/tests/perfrecords.test.ts +257 -0
- package/tests/permdiff.test.ts +85 -0
- package/tests/phishguard.test.ts +128 -0
- package/tests/pickerviews.test.ts +155 -0
- package/tests/pipelines.test.ts +381 -0
- package/tests/planlint.test.ts +240 -0
- package/tests/planreview.test.ts +383 -0
- package/tests/platformtargets.test.ts +56 -0
- package/tests/policy.test.ts +8284 -0
- package/tests/portability.test.ts +96 -0
- package/tests/progress.test.ts +1036 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/protocol.test.ts +4793 -0
- package/tests/publishingpipeline.test.ts +314 -0
- package/tests/quickactions.test.ts +164 -0
- package/tests/readiness.test.ts +78 -0
- package/tests/recipes.test.ts +163 -0
- package/tests/redactshots.test.ts +216 -0
- package/tests/registernvidiakeys.ts +51 -0
- package/tests/relayserve.test.ts +304 -0
- package/tests/releasenotes.test.ts +276 -0
- package/tests/resilience.test.ts +467 -0
- package/tests/resourceaware.test.ts +187 -0
- package/tests/runbudget.test.ts +125 -0
- package/tests/runhistory.test.ts +299 -0
- package/tests/runreplay.test.ts +113 -0
- package/tests/runresume.test.ts +184 -0
- package/tests/runstate.test.ts +288 -0
- package/tests/runtimeadapters.test.ts +251 -0
- package/tests/safariskeleton.test.ts +115 -0
- package/tests/sandboxframe.test.ts +196 -0
- package/tests/sbom.test.ts +105 -0
- package/tests/secretvault.test.ts +188 -0
- package/tests/selcache.test.ts +111 -0
- package/tests/sessioninterface.test.ts +791 -0
- package/tests/sessions.test.ts +591 -0
- package/tests/siteprefs.test.ts +228 -0
- package/tests/snapshotdelta.test.ts +165 -0
- package/tests/statusviews.test.ts +237 -0
- package/tests/stepmeter.test.ts +200 -0
- package/tests/streamparse.test.ts +163 -0
- package/tests/stylepruning.test.ts +90 -0
- package/tests/surfaces.test.ts +282 -0
- package/tests/sweep.test.ts +78 -0
- package/tests/syncbridge.test.ts +164 -0
- package/tests/taskqueue.test.ts +206 -0
- package/tests/toolcalls.test.ts +336 -0
- package/tests/toolcatalog.test.ts +201 -0
- package/tests/torture-agent.test.ts +706 -0
- package/tests/torture-auth.test.ts +461 -0
- package/tests/torture-commands.test.ts +895 -0
- package/tests/torture-data.test.ts +923 -0
- package/tests/torture-environments.test.ts +285 -0
- package/tests/torture-export.test.ts +263 -0
- package/tests/torture-http.test.ts +1379 -0
- package/tests/torture-llm.test.ts +808 -0
- package/tests/torture-memory.test.ts +1084 -0
- package/tests/torture-plan.test.ts +829 -0
- package/tests/torture-run.test.ts +626 -0
- package/tests/torture-session.test.ts +928 -0
- package/tests/torture-swarm.test.ts +1245 -0
- package/tests/torture-tools.test.ts +964 -0
- package/tests/torture-workflow.test.ts +2354 -0
- package/tests/tourviews.test.ts +52 -0
- package/tests/transparency.test.ts +147 -0
- package/tests/trigger.test.ts +941 -0
- package/tests/v1sunset.test.ts +428 -0
- package/tests/virtlist.test.ts +94 -0
- package/tests/vision.test.ts +540 -0
- package/tests/vsixpack.test.ts +145 -0
- package/tests/webapi.test.ts +523 -0
- package/tests/workflow.test.ts +1785 -0
- package/tests/workfloweditor.test.ts +759 -0
- package/tests/xpipack.test.ts +128 -0
- package/tools.ts +734 -0
- package/types.ts +9908 -0
- package/umd.ts +9 -0
- package/utils.ts +249 -0
- package/version.ts +8 -0
- package/versionregistry.ts +1310 -0
- package/views.ts +2153 -0
- package/vitest.config.ts +12 -0
- package/web/App.tsx +78 -0
- package/web/README.md +47 -0
- package/web/capacitor.config.ts +46 -0
- package/web/const.ts +19 -0
- package/web/control.shell.tsx +66 -0
- package/web/dashboardpage.ts +554 -0
- package/web/db.ts +493 -0
- package/web/design.html +541 -0
- package/web/gatewayview/Gateway.tsx +345 -0
- package/web/gatewayview/config.ts +1042 -0
- package/web/gatewayview/definition.ts +174 -0
- package/web/icons.ts +15 -0
- package/{manifest.json → web/manifest.json} +1 -1
- package/web/optionspage.ts +691 -0
- package/web/popup.ts +1651 -0
- package/web/schema.prisma +176 -0
- package/web/sidepanel.ts +7941 -0
- package/web/sitemanifest.json +13 -0
- package/web/terminal-ui.tsx +622 -0
- package/web/transparencypage.ts +541 -0
- package/web/vite.config.ts +39 -0
- package/web/workspace.ts +23 -0
- package/workbenchmemory.ts +104 -0
- package/workbenchsession.ts +304 -0
- package/workflow.ts +4495 -0
- package/bridge.d.ts.map +0 -1
- package/bridge.js +0 -1877
- package/bun.d.ts.map +0 -1
- package/bun.js +0 -52329
- package/caps/background.json +0 -712
- package/caps/cli.json +0 -360
- package/caps/library.json +0 -3611
- package/caps/mcp.json +0 -126
- package/caps/pagebridge.json +0 -377
- package/caps/popup.json +0 -51
- package/caps/sidepanel.json +0 -522
- package/checksums.txt +0 -142
- package/cli.d.ts.map +0 -1
- package/companion.d.ts.map +0 -1
- package/companion.js +0 -389
- package/crossbrowser.d.ts.map +0 -1
- package/crossbrowser.js +0 -1191
- package/dashdone.d.ts.map +0 -1
- package/dashdone.js +0 -368
- package/deno.d.ts.map +0 -1
- package/deno.js +0 -52325
- package/devthink.umd.js +0 -52346
- package/fixtures/bun.ts +0 -7
- package/fixtures/cjs.js +0 -6
- package/fixtures/deno.ts +0 -6
- package/fixtures/example-org-pagestate.json +0 -22
- package/fixtures/importers/automa-workflow.json +0 -18
- package/fixtures/importers/selenium-side.json +0 -20
- package/fixtures/importers/tabular-plan.csv +0 -7
- package/fixtures/importers/uivision-macro.json +0 -10
- package/fixtures/importers/v1-plan.json +0 -13
- package/fixtures/mcp-client.mjs +0 -14
- package/fixtures/pages/checkout.html +0 -192
- package/fixtures/pages/dashboard.html +0 -110
- package/fixtures/pages/feed.html +0 -113
- package/fixtures/pages/product-grid.html +0 -150
- package/fixtures/plans/form-inventory-plan.json +0 -11
- package/fixtures/plans/release-notes-plan.json +0 -11
- package/fixtures/recipes/agentscompete.json +0 -11
- package/fixtures/recipes/agentsforms.json +0 -11
- package/fixtures/recipes/agentsmonitor.json +0 -11
- package/fixtures/recipes/agentsreview.json +0 -29
- package/fixtures/recipes/agentswarm.json +0 -11
- package/fixtures/recipes/fillcalendar.json +0 -12
- package/fixtures/recipes/fillcheckout.json +0 -13
- package/fixtures/recipes/filldependentdropdowns.json +0 -12
- package/fixtures/recipes/fillfromcsv.json +0 -13
- package/fixtures/recipes/fillloginform.json +0 -12
- package/fixtures/recipes/fillmultistepwizard.json +0 -13
- package/fixtures/recipes/fillregistration.json +0 -12
- package/fixtures/recipes/fillsearchfilters.json +0 -12
- package/fixtures/recipes/filltypeahead.json +0 -11
- package/fixtures/recipes/monitoralerts.json +0 -12
- package/fixtures/recipes/monitoravailability.json +0 -11
- package/fixtures/recipes/monitornetworkquiet.json +0 -12
- package/fixtures/recipes/monitorpagechanges.json +0 -11
- package/fixtures/recipes/monitorpricechange.json +0 -11
- package/fixtures/recipes/monitorschedule.json +0 -12
- package/fixtures/recipes/scrapedynamicprices.json +0 -13
- package/fixtures/recipes/scrapeimagegallery.json +0 -12
- package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
- package/fixtures/recipes/scrapenestedlists.json +0 -12
- package/fixtures/recipes/scrapeproductgrid.json +0 -12
- package/fixtures/recipes/scrapereviews.json +0 -12
- package/fixtures/recipes/scrapesearchresults.json +0 -12
- package/fixtures/recipes/scrapetablepagination.json +0 -12
- package/fixtures/recipes/testaccessibility.json +0 -34
- package/fixtures/recipes/testconsoleerrors.json +0 -12
- package/fixtures/recipes/testdeeplinks.json +0 -12
- package/fixtures/recipes/testformvalidation.json +0 -13
- package/fixtures/recipes/testlayoutshift.json +0 -12
- package/fixtures/recipes/testlinkcheck.json +0 -12
- package/fixtures/recipes/testloadtiming.json +0 -12
- package/fixtures/recipes/testvisualdiff.json +0 -12
- package/fixtures/workflows/changelog-digest-workflow.json +0 -13
- package/fixtures/workflows/form-survey-workflow.json +0 -13
- package/gallery.json +0 -706
- package/hardening.d.ts.map +0 -1
- package/hardening.js +0 -188
- package/headless.d.ts.map +0 -1
- package/headless.js +0 -11522
- package/http.d.ts.map +0 -1
- package/http.js +0 -2165
- package/index.cjs +0 -54682
- package/index.d.ts.map +0 -1
- package/index.js +0 -52310
- package/index.neutral.js +0 -46343
- package/mcp.d.ts.map +0 -1
- package/mcp.js +0 -20515
- package/memory.d.ts.map +0 -1
- package/nativehost.template.json +0 -10
- package/node.cjs +0 -54686
- package/node.d.ts.map +0 -1
- package/pack.d.ts.map +0 -1
- package/pack.js +0 -805
- package/policy.d.ts.map +0 -1
- package/progress.d.ts.map +0 -1
- package/progress.js +0 -645
- package/protocol.d.ts.map +0 -1
- package/protocol.js +0 -8334
- package/schemas/audit.schema.json +0 -226
- package/schemas/capability.schema.json +0 -113
- package/schemas/envelope.schema.json +0 -162
- package/schemas/memory.schema.json +0 -51
- package/schemas/observation.schema.json +0 -113
- package/schemas/plan.schema.json +0 -208
- package/schemas/progress.schema.json +0 -126
- package/schemas/proposal.schema.json +0 -68
- package/schemas/session.schema.json +0 -82
- package/schemas/tool.schema.json +0 -94
- package/server.d.ts.map +0 -1
- package/server.js +0 -28877
- package/umd-example.html +0 -18
- package/umd.d.ts.map +0 -1
- /package/{bun.d.ts → dist/bun.d.ts} +0 -0
- /package/{cli.d.ts → dist/cli.d.ts} +0 -0
- /package/{companion.d.ts → dist/companion.d.ts} +0 -0
- /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
- /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
- /package/{deno.d.ts → dist/deno.d.ts} +0 -0
- /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
- /package/{headless.d.ts → dist/headless.d.ts} +0 -0
- /package/{http.d.ts → dist/http.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
- /package/{memory.d.ts → dist/memory.d.ts} +0 -0
- /package/{node.d.ts → dist/node.d.ts} +0 -0
- /package/{pack.d.ts → dist/pack.d.ts} +0 -0
- /package/{policy.d.ts → dist/policy.d.ts} +0 -0
- /package/{progress.d.ts → dist/progress.d.ts} +0 -0
- /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
- /package/{server.d.ts → dist/server.d.ts} +0 -0
- /package/{umd.d.ts → dist/umd.d.ts} +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The version one sunset notice and the one time migration prompt of the 2.0.0 release candidate (roadmap rc.2 items 50, 51 and 52).
|
|
5
|
+
* The harness drives the real background module under a fake chrome: the inbound mcp frame intake marks the once-per-session sunset state a below floor negotiate frame refuses, the onMessage router answers the popup dismissal, the onInstalled update branch fires the migration prompt once through the notification path the trigger notifications already use, and the popup module renders both banner cards with the migration guide reference.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fakeextensionid = "devthinkfaketest0000000000000";
|
|
9
|
+
|
|
10
|
+
/** One recorded browser notification the fake chrome create call kept. */
|
|
11
|
+
type fakenotification = { id: string; options: Record<string, unknown> };
|
|
12
|
+
|
|
13
|
+
/** The shared fake chrome state: the storage map the background memory adapter reads and writes, the notification recorder, the message recorder and the listener registry the tests fire. */
|
|
14
|
+
type fakechromestate = {
|
|
15
|
+
storage: Map<string, unknown>;
|
|
16
|
+
notifications: fakenotification[];
|
|
17
|
+
messages: Array<Record<string, unknown>>;
|
|
18
|
+
clipboardwrites: unknown[][];
|
|
19
|
+
listeners: Record<
|
|
20
|
+
string,
|
|
21
|
+
Array<(payload: unknown, sender: unknown, sendresponse: (value: unknown) => void) => unknown>
|
|
22
|
+
>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** One fake dom node: the popup renders into stub nodes because the popup harness runs in node without a dom. */
|
|
26
|
+
class fakenode {
|
|
27
|
+
textContent = "";
|
|
28
|
+
className = "";
|
|
29
|
+
hidden = false;
|
|
30
|
+
value = "";
|
|
31
|
+
disabled = false;
|
|
32
|
+
type = "button";
|
|
33
|
+
title = "";
|
|
34
|
+
children: fakenode[] = [];
|
|
35
|
+
removed = false;
|
|
36
|
+
readonly attributes: Record<string, string> = {};
|
|
37
|
+
readonly dataset: Record<string, string> = {};
|
|
38
|
+
readonly listeners: Record<string, Array<() => void>> = {};
|
|
39
|
+
addEventListener(kind: string, listener: () => void): void {
|
|
40
|
+
(this.listeners[kind] ??= []).push(listener);
|
|
41
|
+
}
|
|
42
|
+
removeEventListener(): void {
|
|
43
|
+
/* the popup harness never detaches mid test */
|
|
44
|
+
}
|
|
45
|
+
appendChild(child: fakenode): fakenode {
|
|
46
|
+
this.children.push(child);
|
|
47
|
+
return child;
|
|
48
|
+
}
|
|
49
|
+
append(...nodes: Array<fakenode | string>): void {
|
|
50
|
+
for (const node of nodes)
|
|
51
|
+
this.children.push(node instanceof fakenode ? node : Object.assign(new fakenode(), { textContent: node }));
|
|
52
|
+
}
|
|
53
|
+
replaceChildren(...nodes: fakenode[]): void {
|
|
54
|
+
this.children = nodes;
|
|
55
|
+
}
|
|
56
|
+
setAttribute(name: string, value: string): void {
|
|
57
|
+
this.attributes[name] = value;
|
|
58
|
+
}
|
|
59
|
+
remove(): void {
|
|
60
|
+
this.removed = true;
|
|
61
|
+
}
|
|
62
|
+
focus(): void {
|
|
63
|
+
/* the popup harness keeps focus calls inert */
|
|
64
|
+
}
|
|
65
|
+
/** Collects the text of the node tree so banner assertions read the rendered card. */
|
|
66
|
+
textof(): string {
|
|
67
|
+
return `${this.textContent} ${this.children.map((child) => child.textof()).join(" ")}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The fake document the popup module queries: every selector answers an inert stub node and created nodes record into the body. */
|
|
72
|
+
function fakedocument(state: fakechromestate): { body: fakenode; created: fakenode[] } {
|
|
73
|
+
const body = new fakenode();
|
|
74
|
+
const created: fakenode[] = [];
|
|
75
|
+
const documentstub = {
|
|
76
|
+
body,
|
|
77
|
+
querySelector: (): fakenode => new fakenode(),
|
|
78
|
+
createElement: (): fakenode => {
|
|
79
|
+
const node = new fakenode();
|
|
80
|
+
created.push(node);
|
|
81
|
+
return node;
|
|
82
|
+
},
|
|
83
|
+
addEventListener: (): void => {
|
|
84
|
+
/* the popup keydown handler stays inert under the harness */
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
Object.defineProperty(globalThis, "document", { value: documentstub, configurable: true });
|
|
88
|
+
void state;
|
|
89
|
+
return { body, created };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Installs the fake chrome the background and popup modules run against: local storage over a map, the runtime listener registry, the tabs and windows seams the executors read, the notification recorder and the clipboard write recorder. */
|
|
93
|
+
function installfakechrome(): fakechromestate {
|
|
94
|
+
const state: fakechromestate = {
|
|
95
|
+
storage: new Map(),
|
|
96
|
+
notifications: [],
|
|
97
|
+
messages: [],
|
|
98
|
+
clipboardwrites: [],
|
|
99
|
+
listeners: {},
|
|
100
|
+
};
|
|
101
|
+
const on = (name: string) => ({
|
|
102
|
+
addListener: (callback: (payload: never, sender: never, sendresponse: (value: never) => void) => unknown) => {
|
|
103
|
+
(state.listeners[name] ??= []).push(callback as never);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const manifest = {
|
|
107
|
+
version: "2.0.0",
|
|
108
|
+
name: "devthink",
|
|
109
|
+
permissions: ["storage"],
|
|
110
|
+
optional_permissions: [],
|
|
111
|
+
optional_host_permissions: [],
|
|
112
|
+
};
|
|
113
|
+
const chromeapi = {
|
|
114
|
+
runtime: {
|
|
115
|
+
id: fakeextensionid,
|
|
116
|
+
getURL: (path: string) => `chrome-extension://${fakeextensionid}/${path}`,
|
|
117
|
+
getManifest: () => manifest,
|
|
118
|
+
onMessage: on("message"),
|
|
119
|
+
onStartup: on("startup"),
|
|
120
|
+
onInstalled: on("installed"),
|
|
121
|
+
onConnect: on("connect"),
|
|
122
|
+
sendMessage: async (message: Record<string, unknown>) => {
|
|
123
|
+
state.messages.push(message);
|
|
124
|
+
return { ok: true, value: {} };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
tabs: {
|
|
128
|
+
onUpdated: on("tabsupdated"),
|
|
129
|
+
onActivated: on("tabsactivated"),
|
|
130
|
+
onRemoved: on("tabsremoved"),
|
|
131
|
+
captureVisibleTab: async () => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==",
|
|
132
|
+
},
|
|
133
|
+
windows: { WINDOW_ID_CURRENT: -2 },
|
|
134
|
+
storage: {
|
|
135
|
+
local: {
|
|
136
|
+
get: async (key: string) => ({ [key]: state.storage.get(key) }),
|
|
137
|
+
set: async (items: Record<string, unknown>) => {
|
|
138
|
+
for (const [key, value] of Object.entries(items)) state.storage.set(key, value);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
action: { setBadgeText: async () => undefined },
|
|
143
|
+
notifications: {
|
|
144
|
+
create: (id: string, options: Record<string, unknown>) => {
|
|
145
|
+
state.notifications.push({ id, options });
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
permissions: { contains: async () => true, request: async () => true },
|
|
149
|
+
};
|
|
150
|
+
(globalThis as unknown as Record<string, unknown>).chrome = chromeapi;
|
|
151
|
+
(globalThis as unknown as Record<string, unknown>).self = { addEventListener: () => undefined };
|
|
152
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
153
|
+
value: {
|
|
154
|
+
onLine: true,
|
|
155
|
+
clipboard: {
|
|
156
|
+
write: async (items: unknown[]) => {
|
|
157
|
+
state.clipboardwrites.push(items);
|
|
158
|
+
},
|
|
159
|
+
readText: async () => "",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
configurable: true,
|
|
163
|
+
});
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Fires the onInstalled listener the background router registered with the chrome contract shape: the runtime detail, the inert sender and an unused sendresponse keep the typed signature honest. */
|
|
168
|
+
function fireinstalled(state: fakechromestate, detail: { reason: string; previousVersion?: string }): void {
|
|
169
|
+
const listener = state.listeners.installed?.[0];
|
|
170
|
+
if (listener === undefined) throw new Error("The background router registered no onInstalled listener.");
|
|
171
|
+
listener(
|
|
172
|
+
detail,
|
|
173
|
+
{ id: fakeextensionid, url: `chrome-extension://${fakeextensionid}/background.html` },
|
|
174
|
+
() => undefined,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Lets the module load side effects of the background service worker settle before the test asserts on the storage. */
|
|
179
|
+
async function settled(): Promise<void> {
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Fires one recorded listener and awaits the sendresponse callback the chrome messaging contract answers through. */
|
|
184
|
+
async function firemessage(
|
|
185
|
+
state: fakechromestate,
|
|
186
|
+
message: Record<string, unknown>,
|
|
187
|
+
): Promise<{ ok: boolean; value?: unknown; error?: string }> {
|
|
188
|
+
const listener = state.listeners.message?.[0];
|
|
189
|
+
if (listener === undefined) throw new Error("The background router registered no onMessage listener.");
|
|
190
|
+
return await new Promise((resolve) => {
|
|
191
|
+
listener(message, { id: fakeextensionid, url: `chrome-extension://${fakeextensionid}/popup.html` }, (value) =>
|
|
192
|
+
resolve(value as { ok: boolean; value?: unknown; error?: string }),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The below floor negotiate frame a version one client sends. */
|
|
198
|
+
const v1frame = JSON.stringify({
|
|
199
|
+
jsonrpc: "2.0",
|
|
200
|
+
id: 7,
|
|
201
|
+
method: "negotiate",
|
|
202
|
+
params: { capabilities: { protocolmajor: 1 } },
|
|
203
|
+
});
|
|
204
|
+
/** The frozen major two negotiate frame an agreeing client sends. */
|
|
205
|
+
const v2frame = JSON.stringify({
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id: 8,
|
|
208
|
+
method: "negotiate",
|
|
209
|
+
params: { capabilities: { protocolmajor: 2 } },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("the version one sunset notice of the negotiation banner", () => {
|
|
213
|
+
it("marks the once-per-session state when a below floor negotiate frame refuses", async () => {
|
|
214
|
+
const state = installfakechrome();
|
|
215
|
+
vi.resetModules();
|
|
216
|
+
const background = await import("../background.js");
|
|
217
|
+
await settled();
|
|
218
|
+
state.storage.set("mcpstate", { state: "running", startedat: Date.now() });
|
|
219
|
+
const refusal = await background.processmcpframe(v1frame, "", "stdio");
|
|
220
|
+
expect(refusal.error?.message).toContain("below the supported floor");
|
|
221
|
+
expect(refusal.error?.message).toContain("migrateplan");
|
|
222
|
+
expect(refusal.error?.message).toContain("docs/migrationguide.md");
|
|
223
|
+
const marked = state.storage.get("v1sunset") as {
|
|
224
|
+
at: number;
|
|
225
|
+
declared: number;
|
|
226
|
+
clientid: string;
|
|
227
|
+
dismissedat?: number;
|
|
228
|
+
};
|
|
229
|
+
expect(marked.declared).toBe(1);
|
|
230
|
+
expect(marked.dismissedat).toBeUndefined();
|
|
231
|
+
expect(marked.clientid).toContain("client-");
|
|
232
|
+
/* a second refusal of the same session changes nothing: the once-per-session flag holds */
|
|
233
|
+
const second = await background.processmcpframe(v1frame, "", "stdio");
|
|
234
|
+
expect(second.error?.message).toContain("below the supported floor");
|
|
235
|
+
expect(state.storage.get("v1sunset")).toEqual(marked);
|
|
236
|
+
/* a frozen major two frame refuses nothing and marks nothing */
|
|
237
|
+
await background.processmcpframe(v2frame, "", "stdio");
|
|
238
|
+
expect(state.storage.get("v1sunset")).toEqual(marked);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("answers the popup dismissal through the surface command family and resets the banner state for the session", async () => {
|
|
242
|
+
const state = installfakechrome();
|
|
243
|
+
vi.resetModules();
|
|
244
|
+
const background = await import("../background.js");
|
|
245
|
+
await settled();
|
|
246
|
+
state.storage.set("mcpstate", { state: "running", startedat: Date.now() });
|
|
247
|
+
await background.processmcpframe(v1frame, "", "stdio");
|
|
248
|
+
const reply = await firemessage(state, { kind: "surface", v1sunset: { dismiss: true } });
|
|
249
|
+
expect(reply.ok).toBe(true);
|
|
250
|
+
expect((reply.value as { visible: boolean }).visible).toBe(false);
|
|
251
|
+
const dismissed = state.storage.get("v1sunset") as {
|
|
252
|
+
at: number;
|
|
253
|
+
declared: number;
|
|
254
|
+
clientid: string;
|
|
255
|
+
dismissedat?: number;
|
|
256
|
+
};
|
|
257
|
+
expect(dismissed.dismissedat).toBeGreaterThan(0);
|
|
258
|
+
/* a later below floor frame of the same session keeps the dismissal: the banner stays reset for the session */
|
|
259
|
+
await background.processmcpframe(v1frame, "", "stdio");
|
|
260
|
+
expect((state.storage.get("v1sunset") as { dismissedat?: number }).dismissedat).toBe(dismissed.dismissedat);
|
|
261
|
+
const audittrail = state.storage.get("audit") as Array<{ kind: string; summary: string }>;
|
|
262
|
+
expect(
|
|
263
|
+
audittrail.some(
|
|
264
|
+
(entry) => entry.kind === "protocol" && /version one sunset notice marked the session/.test(entry.summary),
|
|
265
|
+
),
|
|
266
|
+
).toBe(true);
|
|
267
|
+
expect(
|
|
268
|
+
audittrail.some(
|
|
269
|
+
(entry) => entry.kind === "surface" && /dismissed the version one sunset banner/.test(entry.summary),
|
|
270
|
+
),
|
|
271
|
+
).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("renders the banner card in the popup with the sunset text, the guide reference and the dismissal wiring", async () => {
|
|
275
|
+
const state = installfakechrome();
|
|
276
|
+
const dom = fakedocument(state);
|
|
277
|
+
vi.resetModules();
|
|
278
|
+
const popup = await import("../web/popup.js");
|
|
279
|
+
const marked = { at: Date.now(), declared: 1, clientid: "client-1" };
|
|
280
|
+
const card = popup.v1sunsetbannercard(marked);
|
|
281
|
+
expect(card.visible).toBe(true);
|
|
282
|
+
expect(card.title).toContain("Protocol v1 retired");
|
|
283
|
+
expect(card.body).toContain("Protocol v1 retired — this client cannot connect.");
|
|
284
|
+
expect(card.body).toContain("The migration guide and the migrateplan command upgrade v1 plans.");
|
|
285
|
+
expect(card.guide).toBe("docs/migrationguide.md");
|
|
286
|
+
expect(card.command).toContain("migrateplan");
|
|
287
|
+
expect(popup.v1sunsetbannercard({ ...marked, dismissedat: Date.now() }).visible).toBe(false);
|
|
288
|
+
expect(popup.v1sunsetbannercard(undefined).visible).toBe(false);
|
|
289
|
+
/* the rendered card carries the sunset text and the dismiss button routes the surface dismissal */
|
|
290
|
+
popup.renderv1sunsetbanner(marked);
|
|
291
|
+
expect(dom.body.children).toContain(
|
|
292
|
+
popup.renderv1sunsetbanner.name === "renderv1sunsetbanner"
|
|
293
|
+
? dom.body.children[dom.body.children.length - 1]
|
|
294
|
+
: dom.body.children[dom.body.children.length - 1],
|
|
295
|
+
);
|
|
296
|
+
const section = dom.body.children[dom.body.children.length - 1] as fakenode & { textof(): string };
|
|
297
|
+
expect(section.textof()).toContain("Protocol v1 retired");
|
|
298
|
+
expect(section.textof()).toContain("docs/migrationguide.md");
|
|
299
|
+
expect(section.textof()).toContain("migrateplan");
|
|
300
|
+
const dismiss = section.children.find((child) => (child.listeners.click ?? []).length > 0);
|
|
301
|
+
expect(dismiss).toBeDefined();
|
|
302
|
+
(dismiss as fakenode).listeners.click?.[0]?.();
|
|
303
|
+
await settled();
|
|
304
|
+
expect(
|
|
305
|
+
state.messages.some(
|
|
306
|
+
(message) =>
|
|
307
|
+
message.kind === "surface" && (message as { v1sunset?: { dismiss?: boolean } }).v1sunset?.dismiss === true,
|
|
308
|
+
),
|
|
309
|
+
).toBe(true);
|
|
310
|
+
popup.renderv1sunsetbanner({ ...marked, dismissedat: Date.now() });
|
|
311
|
+
expect(section.removed).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("the one time version one migration prompt", () => {
|
|
316
|
+
it("prompts once on an update from a 1.x release through the notification path the triggers already use", async () => {
|
|
317
|
+
const state = installfakechrome();
|
|
318
|
+
vi.resetModules();
|
|
319
|
+
const background = await import("../background.js");
|
|
320
|
+
await settled();
|
|
321
|
+
fireinstalled(state, { reason: "update", previousVersion: "1.1.99" });
|
|
322
|
+
await settled();
|
|
323
|
+
expect(state.notifications).toHaveLength(1);
|
|
324
|
+
const prompt = state.notifications[0] as fakenotification;
|
|
325
|
+
expect(prompt.options.title).toBe("Devthink migration prompt");
|
|
326
|
+
expect(String(prompt.options.message)).toContain("migrateplan");
|
|
327
|
+
expect(String(prompt.options.message)).toContain("docs/migrationguide.md");
|
|
328
|
+
const record = state.storage.get("migrationprompt") as {
|
|
329
|
+
promptedat: number;
|
|
330
|
+
previousversion: string;
|
|
331
|
+
command: string;
|
|
332
|
+
migrationpromptdismissed?: boolean;
|
|
333
|
+
dismissedat?: number;
|
|
334
|
+
};
|
|
335
|
+
expect(record.previousversion).toBe("1.1.99");
|
|
336
|
+
expect(record.command).toContain("devthink migrateplan");
|
|
337
|
+
expect(record.migrationpromptdismissed).toBeUndefined();
|
|
338
|
+
const history = state.storage.get("notificationhistory") as Array<{
|
|
339
|
+
title: string;
|
|
340
|
+
body: string;
|
|
341
|
+
deeplink: string;
|
|
342
|
+
}>;
|
|
343
|
+
expect(history).toHaveLength(1);
|
|
344
|
+
expect(history[0]?.body).toContain("migrateplan");
|
|
345
|
+
expect(history[0]?.deeplink).toContain("devthink://migration");
|
|
346
|
+
/* the flag gates the second firing: a second update of the same install prompts nothing */
|
|
347
|
+
fireinstalled(state, { reason: "update", previousVersion: "1.1.99" });
|
|
348
|
+
await settled();
|
|
349
|
+
expect(state.notifications).toHaveLength(1);
|
|
350
|
+
expect(state.storage.get("notificationhistory") as unknown[]).toHaveLength(1);
|
|
351
|
+
/* an update that already sits on the 2.x line prompts nothing and writes no record */
|
|
352
|
+
state.storage.delete("migrationprompt");
|
|
353
|
+
state.notifications.length = 0;
|
|
354
|
+
fireinstalled(state, { reason: "update", previousVersion: "2.0.0" });
|
|
355
|
+
await settled();
|
|
356
|
+
expect(state.notifications).toHaveLength(0);
|
|
357
|
+
expect(state.storage.get("migrationprompt")).toBeUndefined();
|
|
358
|
+
/* an unknowable previous version answers from the stored lastpermissions install marker */
|
|
359
|
+
state.storage.set("lastpermissions", { permissions: ["required:storage"], version: "1.1.95" });
|
|
360
|
+
fireinstalled(state, { reason: "update" });
|
|
361
|
+
await settled();
|
|
362
|
+
expect(state.notifications).toHaveLength(1);
|
|
363
|
+
expect((state.storage.get("migrationprompt") as { previousversion: string }).previousversion).toBe("1.1.95");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("keeps a dismissed prompt from ever appearing again through the persistent flag", async () => {
|
|
367
|
+
const state = installfakechrome();
|
|
368
|
+
vi.resetModules();
|
|
369
|
+
const background = await import("../background.js");
|
|
370
|
+
await settled();
|
|
371
|
+
fireinstalled(state, { reason: "update", previousVersion: "1.1.98" });
|
|
372
|
+
await settled();
|
|
373
|
+
expect(state.notifications).toHaveLength(1);
|
|
374
|
+
const reply = await firemessage(state, { kind: "surface", migrationprompt: { dismiss: true } });
|
|
375
|
+
expect(reply.ok).toBe(true);
|
|
376
|
+
expect((reply.value as { visible: boolean }).visible).toBe(false);
|
|
377
|
+
const dismissed = state.storage.get("migrationprompt") as {
|
|
378
|
+
migrationpromptdismissed?: boolean;
|
|
379
|
+
dismissedat?: number;
|
|
380
|
+
};
|
|
381
|
+
expect(dismissed.migrationpromptdismissed).toBe(true);
|
|
382
|
+
expect(dismissed.dismissedat).toBeGreaterThan(0);
|
|
383
|
+
fireinstalled(state, { reason: "update", previousVersion: "1.1.98" });
|
|
384
|
+
await settled();
|
|
385
|
+
expect(state.notifications).toHaveLength(1);
|
|
386
|
+
const migrate = await firemessage(state, { kind: "surface", migrationprompt: { migrate: true } });
|
|
387
|
+
expect((migrate.value as { command: string }).command).toContain("devthink migrateplan");
|
|
388
|
+
expect((migrate.value as { guide: string }).guide).toBe("docs/migrationguide.md");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("renders the migration prompt card in the popup with the migrate action and the persistent dismissal wiring", async () => {
|
|
392
|
+
const state = installfakechrome();
|
|
393
|
+
const dom = fakedocument(state);
|
|
394
|
+
vi.resetModules();
|
|
395
|
+
const popup = await import("../web/popup.js");
|
|
396
|
+
const prompted = {
|
|
397
|
+
promptedat: Date.now(),
|
|
398
|
+
previousversion: "1.1.99",
|
|
399
|
+
command: "devthink migrateplan <plan source> --format v1 --out <converted plan>",
|
|
400
|
+
};
|
|
401
|
+
const card = popup.migrationpromptcard(prompted);
|
|
402
|
+
expect(card.visible).toBe(true);
|
|
403
|
+
expect(card.title).toContain("Version one migration");
|
|
404
|
+
expect(card.body).toContain("1.1.99");
|
|
405
|
+
expect(card.guide).toBe("docs/migrationguide.md");
|
|
406
|
+
expect(card.command).toContain("--format v1");
|
|
407
|
+
expect(card.migrate).toBe("Migrate");
|
|
408
|
+
expect(popup.migrationpromptcard({ ...prompted, migrationpromptdismissed: true }).visible).toBe(false);
|
|
409
|
+
expect(popup.migrationpromptcard(undefined).visible).toBe(false);
|
|
410
|
+
popup.rendermigrationpromptbanner(prompted);
|
|
411
|
+
const section = dom.body.children[dom.body.children.length - 1] as fakenode & { textof(): string };
|
|
412
|
+
expect(section.textof()).toContain("Version one migration");
|
|
413
|
+
expect(section.textof()).toContain("docs/migrationguide.md");
|
|
414
|
+
const buttons = section.children.filter((child) => (child.listeners.click ?? []).length > 0);
|
|
415
|
+
expect(buttons).toHaveLength(2);
|
|
416
|
+
(buttons[1] as fakenode).listeners.click?.[0]?.();
|
|
417
|
+
await settled();
|
|
418
|
+
expect(
|
|
419
|
+
state.messages.some(
|
|
420
|
+
(message) =>
|
|
421
|
+
message.kind === "surface" &&
|
|
422
|
+
(message as { migrationprompt?: { dismiss?: boolean } }).migrationprompt?.dismiss === true,
|
|
423
|
+
),
|
|
424
|
+
).toBe(true);
|
|
425
|
+
popup.rendermigrationpromptbanner({ ...prompted, migrationpromptdismissed: true });
|
|
426
|
+
expect(section.removed).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
heightmapkey,
|
|
5
|
+
heightmaprecord,
|
|
6
|
+
measurevirtlistrows,
|
|
7
|
+
openvirtlist,
|
|
8
|
+
scrollvirtlist,
|
|
9
|
+
virtlistheight,
|
|
10
|
+
virtlistrows,
|
|
11
|
+
virtlistsavings,
|
|
12
|
+
} from "../views.js";
|
|
13
|
+
import { virtlistwindowvalid } from "../policy.js";
|
|
14
|
+
|
|
15
|
+
describe("virtlist windowing and height maps", () => {
|
|
16
|
+
it("opens the window at scroll top with the user configured row window", () => {
|
|
17
|
+
const window = openvirtlist({ surface: "datagrid", total: 100, rows: 20 });
|
|
18
|
+
expect(window).toEqual({ surface: "datagrid", start: 0, end: 20, total: 100, heights: {}, recycled: 0 });
|
|
19
|
+
const unbounded = openvirtlist({ surface: "logstream", total: 40 });
|
|
20
|
+
expect(unbounded.end).toBe(40);
|
|
21
|
+
expect(() => openvirtlist({ surface: " ", total: 10 })).toThrow(/surface name/i);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("moves the window on scroll, clamps inside the row range and recycles the overlapping rows", () => {
|
|
25
|
+
const window = openvirtlist({ surface: "datagrid", total: 100, rows: 20 });
|
|
26
|
+
const scrolled = scrollvirtlist(window, 30);
|
|
27
|
+
expect(scrolled.start).toBe(30);
|
|
28
|
+
expect(scrolled.end).toBe(50);
|
|
29
|
+
expect(scrolled.recycled).toBe(0);
|
|
30
|
+
const next = scrollvirtlist(scrolled, 40);
|
|
31
|
+
expect(next.start).toBe(40);
|
|
32
|
+
expect(next.end).toBe(60);
|
|
33
|
+
expect(next.recycled).toBe(10);
|
|
34
|
+
const clampedtop = scrollvirtlist(next, -50);
|
|
35
|
+
expect(clampedtop.start).toBe(0);
|
|
36
|
+
const clampedbottom = scrollvirtlist(next, 1000);
|
|
37
|
+
expect(clampedbottom.start).toBe(80);
|
|
38
|
+
expect(clampedbottom.end).toBe(100);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("measures the row heights into the height map and reuses the stored heights", () => {
|
|
42
|
+
let window = openvirtlist({ surface: "compareviewer", total: 10, rows: 5 });
|
|
43
|
+
window = measurevirtlistrows(window, { "compareviewer:0": 24, "compareviewer:1": 32 });
|
|
44
|
+
expect(window.heights).toEqual({ "compareviewer:0": 24, "compareviewer:1": 32 });
|
|
45
|
+
window = measurevirtlistrows(window, { "compareviewer:2": 28 });
|
|
46
|
+
expect(Object.keys(window.heights)).toHaveLength(3);
|
|
47
|
+
const height = virtlistheight(window, 20);
|
|
48
|
+
expect(height.measured).toBe(84);
|
|
49
|
+
expect(height.total).toBe(84 + 7 * 20);
|
|
50
|
+
expect(heightmapkey("compareviewer", 2)).toBe("compareviewer:2");
|
|
51
|
+
const record = heightmaprecord({ surface: "compareviewer", heights: window.heights, now: 1 });
|
|
52
|
+
expect(record.surface).toBe("compareviewer");
|
|
53
|
+
expect(() => heightmaprecord({ surface: " ", heights: {}, now: 1 })).toThrow(/surface name/i);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders only the window slice while the full list stays in memory", () => {
|
|
57
|
+
const rows = Array.from({ length: 10 }, (_, index) => `row${index}`);
|
|
58
|
+
const window = openvirtlist({ surface: "logstream", total: rows.length, rows: 3 });
|
|
59
|
+
expect(virtlistrows(window, rows)).toEqual(["row0", "row1", "row2"]);
|
|
60
|
+
const scrolled = scrollvirtlist(window, 8);
|
|
61
|
+
expect(virtlistrows(scrolled, rows)).toEqual(["row7", "row8", "row9"]);
|
|
62
|
+
const savings = virtlistsavings(scrolled);
|
|
63
|
+
expect(savings).toEqual({ rendered: 3, skipped: 7, recycled: scrolled.recycled });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("validates the row window as a user choice with no engine default", () => {
|
|
67
|
+
expect(virtlistwindowvalid({ surface: "datagrid", rows: 20 }).allowed).toBe(true);
|
|
68
|
+
expect(virtlistwindowvalid({ surface: "datagrid" }).allowed).toBe(true);
|
|
69
|
+
expect(virtlistwindowvalid({ surface: "datagrid" }).reason).toMatch(/renders every row/i);
|
|
70
|
+
expect(virtlistwindowvalid({ surface: "datagrid", rows: 0 }).allowed).toBe(false);
|
|
71
|
+
expect(virtlistwindowvalid({ surface: "datagrid", rows: -3 }).allowed).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("stepstimeline adopts the virtlist windowing the logstream and the datagrid render through", async () => {
|
|
75
|
+
/* the 1.1.68 roadmap item 38 adoption: the stepstimeline nodes window through the same virtlist family, so a long plan renders its visible slice while the full step chain stays in memory and the scroll recycles the row nodes */
|
|
76
|
+
const nodes = Array.from({ length: 60 }, (_, index) => `step-${index}`);
|
|
77
|
+
const window = openvirtlist({ surface: "stepstimeline", total: nodes.length, rows: 25 });
|
|
78
|
+
expect(window.surface).toBe("stepstimeline");
|
|
79
|
+
expect(virtlistrows(window, nodes)).toHaveLength(25);
|
|
80
|
+
const scrolled = scrollvirtlist(window, 40);
|
|
81
|
+
expect(virtlistrows(scrolled, nodes)).toEqual(nodes.slice(35, 60));
|
|
82
|
+
expect(virtlistsavings(scrolled)).toEqual({ rendered: 25, skipped: 35, recycled: 0 });
|
|
83
|
+
const unbounded = openvirtlist({ surface: "stepstimeline", total: nodes.length });
|
|
84
|
+
expect(unbounded.end).toBe(nodes.length);
|
|
85
|
+
/* the sidepanel renders the timeline through the same window options family and the same rowsneeded budget seam the logstream renders through: an unset window renders every node while the windowed render notes the slice it shows */
|
|
86
|
+
const sidepanel = await readFile("web/sidepanel.ts", "utf8");
|
|
87
|
+
expect(sidepanel).toContain('virtlist: { open: { surface: "stepstimeline", total: result.nodes.length } } }');
|
|
88
|
+
expect(sidepanel).toContain("windowed = result.nodes.slice(perfview.window.start, perfview.window.end);");
|
|
89
|
+
expect(sidepanel).toContain("} catch { /* an unset window renders every timeline node */ }");
|
|
90
|
+
/* the logstream and the datagrid keep their own adoption of the same family */
|
|
91
|
+
expect(sidepanel).toContain('virtlist: { open: { surface: "logstream", total: liveevents.length } } }');
|
|
92
|
+
expect(sidepanel).toContain('virtlist: { open: { surface: "datagrid", total: rows.length } } }');
|
|
93
|
+
});
|
|
94
|
+
});
|