test(thumbnail): [PKG-TOOL-THUMB-01] freeze recovered release and workspace contracts

This commit is contained in:
Harvey Zhao committed 2026-09-13 03:16:30 +08:00
1 parent cb6c767731
commit 0338e0b8b1
12 files changed
+805 -11

No files matched your search

@@ -0,0 +1,79 @@
{
"schemaVersion": 1,
"task": "PKG-TOOL-THUMB-01",
"verifiedAt": "2026-09-12T19:16:08.007Z",
"startedFrom": "cb6c7677319f22074ae2ec09989022ddf0f8ea87",
"runtime": {
"node": "v24.21.0",
"yarn": "1.22.22",
"platform": "win32"
},
"checks": {
"contract": {
"passed": 26,
"failed": 0,
"skipped": 0
},
"focusedLint": {
"exitCode": 0
},
"ci": {
"exitCode": 0,
"unit": 1365,
"engineering": 14,
"baseline": 191,
"total": 1570,
"productionTs": 330
}
},
"inputs": [
{
"file": "refactor/baselines/thumbnail-release.json",
"sha256": "ad02af2abdb3185598b76a0c617eb5a5ffc89f89a7ef6f01ba8f3db0a87012be",
"algorithm": "sha256-lf"
},
{
"file": "refactor/baselines/thumbnail-contract.md",
"sha256": "7048d504335f4e39c39d833bae36e7a3c6fccc6ad29d475781ca84158ff23278",
"algorithm": "sha256-lf"
},
{
"file": "refactor/scripts/thumbnail-contract.mjs",
"sha256": "14ab6157f14bac00ecc4974f67e7afbe98001e7f6490e94da7839bf4556837fb",
"algorithm": "sha256-lf"
},
{
"file": "refactor/scripts/thumbnail-contract.test.mjs",
"sha256": "77786d54a20a485203812be9145c13f1a9cbca62d3180993e6cc6ee296cb7437",
"algorithm": "sha256-lf"
},
{
"file": "test/helpers/thumbnail.js",
"sha256": "18d3ab1a73867399ec32e99ba2c60a0427289fe9773e8cee6284e1d699520ed8",
"algorithm": "sha256-lf"
}
],
"logs": [
{
"file": "refactor/.cache/thumbnail01-contract.log",
"sha256": "c4b44783bffb1a997614d0ff5677d253a536302a1ef05c4d9fd975fd8effd75b",
"algorithm": "sha256-lf"
},
{
"file": "refactor/.cache/thumbnail01-lint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"algorithm": "sha256-lf"
},
{
"file": "refactor/.cache/thumbnail01-ci.log",
"sha256": "fb0c351f0697efc3059f86f5efb7deb4a2ad25810e4733fc1a3054368c61d76c",
"algorithm": "sha256-lf"
}
],
"limitations": [
"Controlled DOM/media only; no new browser or coverage run.",
"Original npm archive unavailable; recovered CDN main verified byte-for-byte against frozen Git.",
"No production source/build/manifest/type changes; default compatibility decision and resource fixes remain open.",
"Existing generated core editor lint warning and Yarn url.parse deprecation remain; no new focused lint warnings."
]
}
+92
View File
@@ -0,0 +1,92 @@
# Thumbnail tool compatibility baseline
Task: PKG-TOOL-THUMB-01. This is a contract inventory, not completion of the
runtime migration or release acceptance. No production source or build output
changes in this task. Run `node --test refactor/scripts/thumbnail-contract.test.mjs`.
## Evidence and version boundaries
`thumbnail-release.json` freezes 11 HTTP observations, 12 baseline workspace Git
inputs and 8 historical Git inputs. The verifier checks bytes/hashes and core
associations without accessing the network. Registry observations are dated
snapshots, not permanent assertions about package availability.
The registry returned unpublished metadata, listing releases through 3.5.31 and
an unpublish timestamp of 2024-05-26T01:46:10.518Z. Version metadata and original
tarballs for 3.5.31 and 4.4.0 were unavailable. Cached jsDelivr 3.5.31 package.json
and main JS were recovered. The main JS is byte-identical to Git commit
af1a116a31b088efdd77afa81c4b6565d12bd162; its manifest matches that commit except
Git's stale gitHead field. This proves the recovered runtime identity, **not a
complete original npm archive or original registry integrity**. The associated
historical core is 3.5.31, while baseline workspace tool 4.4.0 is associated with
core 5.4.1 at 40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f.
| Surface | Recovered 3.5.31 | Baseline workspace 4.4.0 |
| --- | --- | --- |
| Main | Explicit dist/artplayer-tool-thumbnail.js | No main; require/default exports point to existing JS |
| ESM | No declared ESM entry | Declared .esm.js missing; actual .mjs exists |
| Types | No declaration entry | Declared types/artplayer-tool-thumbnail.d.ts missing |
| CSS | Declared dist/artplayer-tool-thumbnail.css; recovery returned 404 | No style entry |
| Emitter | Declared tiny-emitter ^2.1.0, bundled into recovered main | Local src/emitter.js; provenance review remains open |
| File event | Synchronous, before assigning video.src | Same |
| Video event | After delay (default 300 ms) | Synchronous after assigning video.src |
| Screenshot height | Configured height, clamped 10–1000 | Derived from source aspect ratio at start |
| Frame completion | Configured delay per frame; final delay * 2 | oncanplay per frame; no final delay |
| File input | Does not reset value | Resets event.target.value after load |
| Missing metadata | Timer calls unbound this.start | Timer preserves receiver but can poll indefinitely |
The missing historical CSS observation does not prove it never shipped. The
missing workspace entries are checked against the actual frozen Git tree.
The same constructor call cannot simultaneously preserve both versions' height
and event timing defaults. This difference predates the refactor. Tasks 02–04
must document a concrete compatibility decision before changing these defaults;
there is no user approval to silently reinterpret the recovered version.
## Public surface to preserve
CJS exports the constructor directly, with no self-default property. Script
global is `ArtplayerToolThumbnail`; workspace ESM has only the default export.
The constructor accepts an option object and requires a same-realm Element in
fileInput. A non-file-input element receives a transparent child file input.
Unknown option fields survive setup; number/width/column clamp without rounding.
DEFAULTS returns a fresh object and end is NaN.
Keep historical spellings `creatVideo`, `creatScreenshotDate`, `creatCanvas`.
Other methods: setup, inputChange, ondrop, loadVideo, start, download,
errorHandle, destroy; static ondragover. Initial own fields are processing,
option, video, duration, inputChange and ondrop in that order. file, videoUrl,
thumbnailUrl, density and emitter e appear during use. setup/download and
on/once/emit/off return this; loadVideo/destroy return undefined. start returns
a promise after synchronous preflight, which can emit error and throw.
Events: file(File), video(video element), canvas(canvas), update(blob URL,
progress), done(), download(filename), error(message), destroy(). Error listeners
run before synchronous errors escape. Emitter preserves context, listener
snapshots and removal of once listeners by their original callback.
Grid samples interval midpoints, lays rows by column and appends a 30 px footer
with the existing attribution and geometry. Download replaces the final filename
extension with .png; a filename without a dot currently becomes .png.
## Defects and remaining validation
Historical and workspace tests reproduce undefined static drop listener binding
(the callable method is on the instance), non-idempotent destroy, and odd
extensionless download names. Source review additionally finds unreleased old
video URLs on replacement, no pending extraction/timer cancellation, generated
input ownership gaps and asynchronous callback paths that can remain unsettled.
These are repair candidates, not requirements to perpetuate defects. Task 02
must reproduce asynchronous paths; 03 must pair fixes with regression evidence.
The 26 Node contract cases use controlled DOM/media objects. They establish
export shape, event timing, defaults, geometry and deterministic legacy behavior;
they do not prove native decoding, seek readiness, toBlob failure behavior or
real browser cleanup. Task 02 adds extraction/failure sequences; 05 validates
real media and generated sheets with old/new cores; 06 verifies installed
tarballs, old paths, CSS disposition and the actual docs editor on port 8082.
The tool demo is `docs/assets/example/tool.thumbnail.js`. Current
`docs/assets/example/thumbnail.js` is an external thumbnail plugin and must not
be substituted. The package README's example=thumbnail link needs correction
with final demo verification. Emitter source/license attribution needs evidence
before classifying it as fully self-authored or removing dependency notices.
+172
View File
@@ -0,0 +1,172 @@
{
"schemaVersion": 1,
"task": "PKG-TOOL-THUMB-01",
"capturedAt": "2026-09-12T19:02:10.350Z",
"sourceCommit": "40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f",
"observations": [
{
"url": "https://registry.npmjs.org/artplayer-tool-thumbnail",
"status": 200,
"contentType": "application/json",
"sha256": "1a5bbf4d14852e0dba2ed51351818bddf742a1278b46ef60366a6a544d443bcf",
"bytes": 2552,
"text": "{\"_id\":\"artplayer-tool-thumbnail\",\"name\":\"artplayer-tool-thumbnail\",\"time\":{\"created\":\"2018-12-17T07:19:51.643Z\",\"1.0.6\":\"2018-12-17T07:19:51.763Z\",\"modified\":\"2024-05-26T01:46:10.518Z\",\"1.0.7\":\"2019-04-15T12:31:02.044Z\",\"1.0.8\":\"2019-04-15T12:37:58.551Z\",\"1.0.9\":\"2019-04-16T02:15:59.423Z\",\"3.1.5\":\"2019-04-16T03:47:41.590Z\",\"3.1.6\":\"2019-04-19T09:01:49.496Z\",\"3.1.7\":\"2019-04-22T22:56:08.033Z\",\"3.1.8\":\"2019-05-03T14:22:48.767Z\",\"3.1.9\":\"2019-05-12T22:09:41.935Z\",\"3.1.10\":\"2019-07-19T09:55:45.112Z\",\"3.1.12\":\"2019-07-19T10:25:47.480Z\",\"3.1.13\":\"2019-08-15T00:37:21.970Z\",\"3.1.14\":\"2019-08-21T03:16:24.138Z\",\"3.1.15\":\"2019-09-15T08:17:18.441Z\",\"3.1.16\":\"2019-09-25T04:44:37.557Z\",\"3.1.17\":\"2019-11-10T07:53:32.969Z\",\"3.1.18\":\"2019-11-11T01:22:47.207Z\",\"3.1.19\":\"2019-11-20T01:50:06.770Z\",\"3.2.3\":\"2019-11-22T13:38:56.548Z\",\"3.2.4\":\"2019-11-22T13:42:09.267Z\",\"3.2.5\":\"2019-11-28T03:55:29.655Z\",\"3.2.6\":\"2019-12-02T00:40:50.888Z\",\"3.3.1\":\"2019-12-09T09:38:40.061Z\",\"3.3.2\":\"2019-12-11T09:11:27.149Z\",\"3.3.3\":\"2020-02-24T03:20:49.402Z\",\"3.4.1\":\"2020-02-27T09:05:21.228Z\",\"3.4.2\":\"2020-02-28T01:17:51.271Z\",\"3.5.1\":\"2020-03-21T23:37:20.360Z\",\"3.5.2\":\"2020-04-14T05:00:02.544Z\",\"3.5.4\":\"2020-04-27T03:45:46.217Z\",\"3.5.5\":\"2020-05-20T09:16:01.958Z\",\"3.5.8\":\"2020-05-21T02:21:53.746Z\",\"3.5.9\":\"2020-05-29T14:06:45.737Z\",\"3.5.10\":\"2020-08-27T08:28:59.925Z\",\"3.5.11\":\"2020-09-02T14:10:51.585Z\",\"3.5.12\":\"2020-09-04T00:57:36.451Z\",\"3.5.13\":\"2020-09-04T01:05:29.301Z\",\"3.5.14\":\"2020-09-04T14:41:02.505Z\",\"3.5.15\":\"2020-09-23T07:02:37.076Z\",\"3.5.17\":\"2020-10-18T06:50:21.989Z\",\"3.5.18\":\"2020-11-07T14:19:34.420Z\",\"3.5.19\":\"2020-11-09T13:32:20.716Z\",\"3.5.20\":\"2020-11-21T00:43:09.744Z\",\"3.5.21\":\"2020-11-22T13:22:03.785Z\",\"3.5.22\":\"2020-11-23T12:28:59.918Z\",\"3.5.23\":\"2020-11-24T00:03:18.563Z\",\"3.5.24\":\"2020-11-24T06:11:25.292Z\",\"3.5.26\":\"2020-12-01T04:01:24.276Z\",\"3.5.27\":\"2020-12-17T07:35:02.960Z\",\"3.5.28\":\"2021-05-06T05:10:54.117Z\",\"3.5.29\":\"2021-05-11T02:37:37.309Z\",\"3.5.30\":\"2021-05-20T01:39:24.688Z\",\"3.5.31\":\"2021-11-01T01:38:03.589Z\",\"unpublished\":{\"time\":\"2024-05-26T01:46:10.518Z\",\"versions\":[\"1.0.6\",\"1.0.7\",\"1.0.8\",\"1.0.9\",\"3.1.10\",\"3.1.12\",\"3.1.13\",\"3.1.14\",\"3.1.15\",\"3.1.16\",\"3.1.17\",\"3.1.18\",\"3.1.19\",\"3.1.5\",\"3.1.6\",\"3.1.7\",\"3.1.8\",\"3.1.9\",\"3.2.3\",\"3.2.4\",\"3.2.5\",\"3.2.6\",\"3.3.1\",\"3.3.2\",\"3.3.3\",\"3.4.1\",\"3.4.2\",\"3.5.1\",\"3.5.10\",\"3.5.11\",\"3.5.12\",\"3.5.13\",\"3.5.14\",\"3.5.15\",\"3.5.17\",\"3.5.18\",\"3.5.19\",\"3.5.2\",\"3.5.20\",\"3.5.21\",\"3.5.22\",\"3.5.23\",\"3.5.24\",\"3.5.26\",\"3.5.27\",\"3.5.28\",\"3.5.29\",\"3.5.30\",\"3.5.31\",\"3.5.4\",\"3.5.5\",\"3.5.8\",\"3.5.9\"]}}}"
},
{
"url": "https://registry.npmjs.org/artplayer-tool-thumbnail/4.4.0",
"status": 404,
"contentType": "application/json",
"sha256": "a29f419cca9cebf83537c6e6f756310c63d1452c02a1e49b1a6869b33ef0f982",
"bytes": 26,
"text": "\"version not found: 4.4.0\""
},
{
"url": "https://registry.npmjs.org/artplayer-tool-thumbnail/3.5.31",
"status": 404,
"contentType": "application/json",
"sha256": "7b65582bd77080c46c5a306ef6108709b5b8919a678498b2d9dda8c6fcf9e49e",
"bytes": 27,
"text": "\"version not found: 3.5.31\""
},
{
"url": "https://registry.npmjs.org/artplayer-tool-thumbnail/-/artplayer-tool-thumbnail-3.5.31.tgz",
"status": 404,
"contentType": "application/json",
"sha256": "c8d3eae160a892e32837db3dcae515e843e5383fef52b8141940c8bcf8b6d59f",
"bytes": 21,
"text": "{\"error\":\"Not found\"}"
},
{
"url": "https://registry.npmjs.org/artplayer-tool-thumbnail/-/artplayer-tool-thumbnail-4.4.0.tgz",
"status": 404,
"contentType": "application/json",
"sha256": "c8d3eae160a892e32837db3dcae515e843e5383fef52b8141940c8bcf8b6d59f",
"bytes": 21,
"text": "{\"error\":\"Not found\"}"
},
{
"url": "https://cdn.jsdelivr.net/npm/artplayer-tool-thumbnail@3.5.31/package.json",
"status": 200,
"contentType": "application/json; charset=utf-8",
"sha256": "f9607cb3c5d6edfbfc417b71abdf88148da1ed8ed5d23b03017ba851b2b6c9de",
"bytes": 799,
"text": "{\n \"name\": \"artplayer-tool-thumbnail\",\n \"version\": \"3.5.31\",\n \"expose\": \"ArtplayerToolThumbnail\",\n \"description\": \"thumbnail tool for ArtPlayer\",\n \"main\": \"dist/artplayer-tool-thumbnail.js\",\n \"style\": \"dist/artplayer-tool-thumbnail.css\",\n \"scripts\": {\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/zhw2590582/ArtPlayer.git\"\n },\n \"keywords\": [\n \"video\",\n \"player\"\n ],\n \"author\": \"Harvey Zack <laozhaochaguan@gmail.com>\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/zhw2590582/ArtPlayer/issues\"\n },\n \"homepage\": \"https://github.com/zhw2590582/ArtPlayer#readme\",\n \"dependencies\": {\n \"tiny-emitter\": \"^2.1.0\"\n },\n \"gitHead\": \"af1a116a31b088efdd77afa81c4b6565d12bd162\"\n}\n"
},
{
"url": "https://cdn.jsdelivr.net/npm/artplayer-tool-thumbnail@3.5.31/dist/artplayer-tool-thumbnail.js",
"status": 200,
"contentType": "application/javascript; charset=utf-8",
"sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87",
"bytes": 8716,
"identicalGit": {
"commit": "af1a116a31b088efdd77afa81c4b6565d12bd162",
"file": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js"
}
},
{
"url": "https://cdn.jsdelivr.net/npm/artplayer-tool-thumbnail@3.5.31/dist/artplayer-tool-thumbnail.css",
"status": 404,
"contentType": "text/plain; charset=utf-8",
"sha256": "525a0f43f5b5fa874ba9306afade56063dcf874a59a2769af9efbc9cbf7a32b0",
"bytes": 58,
"text": "Failed to fetch version info for artplayer-tool-thumbnail."
},
{
"url": "https://cdn.jsdelivr.net/npm/artplayer-tool-thumbnail@3.5.31/src/index.js",
"status": 404,
"contentType": "text/plain; charset=utf-8",
"sha256": "525a0f43f5b5fa874ba9306afade56063dcf874a59a2769af9efbc9cbf7a32b0",
"bytes": 58,
"text": "Failed to fetch version info for artplayer-tool-thumbnail."
},
{
"url": "https://cdn.jsdelivr.net/npm/artplayer-tool-thumbnail@4.4.0/package.json",
"status": 404,
"contentType": "text/plain; charset=utf-8",
"sha256": "525a0f43f5b5fa874ba9306afade56063dcf874a59a2769af9efbc9cbf7a32b0",
"bytes": 58,
"text": "Failed to fetch version info for artplayer-tool-thumbnail."
},
{
"url": "https://data.jsdelivr.com/v1/package/npm/artplayer-tool-thumbnail@3.5.31",
"status": 404,
"contentType": "application/json; charset=utf-8",
"sha256": "ca673be59ba098d94b6734968b8e9b6f8c57c3a1b0f03d2a80e97ec0cc599255",
"bytes": 176,
"text": "{\n\t\"status\": 404,\n\t\"message\": \"Couldn't find version 3.5.31 for artplayer-tool-thumbnail. Make sure you use a specific version number, and not a version range or an npm tag.\"\n}"
}
],
"recovered": {
"name": "artplayer-tool-thumbnail",
"version": "3.5.31",
"manifest": {
"name": "artplayer-tool-thumbnail",
"version": "3.5.31",
"expose": "ArtplayerToolThumbnail",
"description": "thumbnail tool for ArtPlayer",
"main": "dist/artplayer-tool-thumbnail.js",
"style": "dist/artplayer-tool-thumbnail.css",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"keywords": [
"video",
"player"
],
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"homepage": "https://github.com/zhw2590582/ArtPlayer#readme",
"dependencies": {
"tiny-emitter": "^2.1.0"
},
"gitHead": "af1a116a31b088efdd77afa81c4b6565d12bd162"
},
"historicalCommit": "af1a116a31b088efdd77afa81c4b6565d12bd162",
"originalTarballAvailable": false,
"completeArchiveAvailable": false,
"scope": "CDN manifest and main runtime recovered; runtime is byte-identical to the matching Git build. Registry manifests/tarballs are unavailable. Missing CSS is an observed unavailable target, not proof it never existed."
},
"source": {
"packages/artplayer-tool-thumbnail/src/index.js": "67bc1b6a58faf11b14c8d410ae829edd64b84dc9e7b4b388b2e2dd60788bcbe4",
"packages/artplayer-tool-thumbnail/src/emitter.js": "5aa71e4317037c06b94e259beed2e964bbbb7117edbe78d29cc941c4916fd2ff",
"packages/artplayer-tool-thumbnail/src/utils.js": "dc19fe2f338df7058084eb2301d37db59ec4c914a85ba819a856d270c1267105",
"packages/artplayer-tool-thumbnail/package.json": "9f4f9c6628ee48068f380bc52f4058c1f6aac31c20de6ed069a2c96ba635e23f",
"packages/artplayer-tool-thumbnail/README.md": "91477fe542a44713089e384308ae6f311f887cfe63aafe1483b778a37acfa458",
"packages/artplayer-tool-thumbnail/.npmignore": "6b340a1f9a1f36dbd84cc1fff61de20f4ab2041d767a996da9f9d8de1a582ae8",
"packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98",
"packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98",
"packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs": "4d46df8cccdc92d18efc6f12abedf85e8881737023f4ead28697c8b1e85eead1",
"docs/assets/example/tool.thumbnail.js": "ef830a5f499e14c09d51aa31aaa0a194ea1f910b27f14ef97c0ca8f67d529a54",
"docs/assets/example/thumbnail.js": "e166845905f131dea0979eb77471f5531a59b93ab2dc28fef1b6791adb42fc30",
"packages/artplayer/package.json": "06cbc0de3f944dfc215ea1c4e7c3376e6352ee5d6f1f459828f45025c62f57f0"
},
"historicalSource": {
"packages/artplayer-tool-thumbnail/src/index.js": "cfd4487f508378fd90f5252c9ae92954b93d04e263ce0927146ff9c3160700e0",
"packages/artplayer-tool-thumbnail/src/utils.js": "c70c6752565aff6aa74232dfcda706b98976d466bad799945b6501579cd7c05c",
"packages/artplayer-tool-thumbnail/package.json": "a06b12475ede33cf24af18896e76af52908fab2e5fe3b4c9395164cbe9a22573",
"packages/artplayer-tool-thumbnail/README.md": "d56adcd606f98b8a9be526954d9874831d89e5ae0e1d8dc070a8afe13a3f0547",
"packages/artplayer-tool-thumbnail/.npmignore": "6b340a1f9a1f36dbd84cc1fff61de20f4ab2041d767a996da9f9d8de1a582ae8",
"packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87",
"docs/assets/example/thumbnail.js": "ad807af57b1c006acf9cb5476f934aed0a77487c82be050b908d3d7743bac8b6",
"packages/artplayer/package.json": "8b67bd5fcd17a5e481dd477cc0e2b8327160a7b5699dab7b22c884c109a494eb"
},
"coreAssociations": {
"historical": {
"commit": "af1a116a31b088efdd77afa81c4b6565d12bd162",
"file": "packages/artplayer/package.json",
"version": "3.5.31"
},
"workspace": {
"commit": "40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f",
"file": "packages/artplayer/package.json",
"version": "5.4.1"
}
}
}
@@ -0,0 +1,26 @@
# PKG-TOOL-THUMB-01 contract inventory
Started from cb6c7677319f22074ae2ec09989022ddf0f8ea87. Frozen published-cache and
workspace identities establish the thumbnail tool's actual public contract.
Recovered CDN main matches historical Git byte-for-byte; original npm tarballs
remain unavailable. No complete-archive verification is claimed.
The contract document distinguishes 3.5.31 delayed events/configured height from
4.4.0 synchronous events/aspect-derived height, including different input reset
and metadata waiting behavior. Default compatibility remains an explicit open
decision for implementation, not an approved runtime break. It corrects the
plan's earlier conflation of workspace missing main/types/ESM with old npm facts.
Added an offline verifier and 26 contract tests across recovered old main,
frozen workspace main/legacy and workspace ESM. Tests cover export/field shape,
historical method spellings, setup/defaults, emitter semantics, event timing,
input/drop, height, grid/footer, errors, downloads and cleanup. The helper uses
controlled DOM/media; real browser extraction is not claimed by this task.
Production source, declarations, manifests and dist/docs artifacts are untouched.
No dependencies changed. PKG-TOOL-THUMB-02 owns extraction and asynchronous failure
baselines; 03/04 own resource/module/TS implementation; 05/06 own real integration
and distribution. BASE-DIST-01 and thumbnail-specific risks remain open.
Validation results are recorded in thumbnail-contract-validation.json. Task
completion uses its own local commit; there is no push, tag or npm publication.
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
状态:todo 103 / doing 10 / blocked 0 / done 109 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 102 / doing 10 / blocked 0 / done 110 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -355,7 +355,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-TOOL-THUMB-01 | artplayer-tool-thumbnail<br>核对包契约与历史用法 | BASE-05 | 构造/defaults/事件/方法/历史拼写、.esm.js 入口和缺失 types | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;核实 BASE-DIST-01 的 .esm.js 旧入口、缺失声明及无 main 的历史发布事实 | H | todo |
| PKG-TOOL-THUMB-01 | artplayer-tool-thumbnail<br>核对包契约与历史用法 | BASE-05 | 构造/defaults/事件/方法/历史拼写、.esm.js 入口和缺失 types | 源码/声明/README/demo/可恢复发布内容差异已登记;公开形状和版本范围冻结;区分旧 npm 的 main/CSS 与当前工作区无 main、.esm.js/类型缺失,不把工作区状态冒称旧 npm 发布事实 | H | done |
| PKG-TOOL-THUMB-02 | artplayer-tool-thumbnail<br>建立特有行为与错误测试 | PKG-TOOL-THUMB-01, ENG-03, ENG-05 | 文件输入/拖放、抽帧网格、begin/end、进度/失败/重复任务 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
| PKG-TOOL-THUMB-03 | artplayer-tool-thumbnail<br>整理内部职责与资源 | PKG-TOOL-THUMB-02, CORE-01 | 输入、抽帧队列、网格导出、URL/监听清理分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-TOOL-THUMB-04 | artplayer-tool-thumbnail<br>迁移自有源码和公开类型 | PKG-TOOL-THUMB-03, ENG-04, ENG-06, CORE-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | todo |
@@ -535,4 +535,5 @@
- PKG-IFRAME-03: [记录](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md) [记录](baselines/iframe-requests-checkpoint.json) [记录](changes/2026-09-12-PKG-IFRAME-03-boundaries-checkpoint.md) [记录](baselines/iframe-boundaries-checkpoint.json) [记录](iframe-message-boundary.md) [记录](changes/2026-09-13-PKG-IFRAME-03-navigation.md) [记录](baselines/iframe-navigation-validation.json) [记录](iframe-document-protocol.md)
- PKG-IFRAME-04: [记录](changes/2026-09-13-PKG-IFRAME-04-types.md) [记录](baselines/iframe-types-validation.json)
- PKG-IFRAME-05: [记录](changes/2026-09-13-PKG-IFRAME-05-integration.md) [记录](baselines/iframe-integration-validation.json) [记录](changes/2026-09-13-PKG-IFRAME-05-history.md) [记录](baselines/iframe-history-validation.json)
- PKG-TOOL-THUMB-01: [记录](baselines/thumbnail-release.json) [记录](baselines/thumbnail-contract.md) [记录](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md) [记录](baselines/thumbnail-contract-validation.json)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
+12
View File
@@ -1,5 +1,17 @@
# 进度与证据
## PKG-TOOL-THUMB-01 完成:历史契约与发布证据
冻结11条HTTP观察、12个工作区及8个历史Git输入;恢复3.5.31 CDN主文件并逐字节
匹配Git,不冒称找回完整npm包。区分旧版delay/固定高度与4.4.0同步事件/比例高度,
保留公开拼写、导出和事件的26项可重跑契约。复现拖放绑定、重复销毁等既有问题;
默认语义、资源治理和分发风险保持open,由02–06继续。实际工具demo与外部插件区分。
完整CI1570项通过,330生产TS;本轮无生产源码/产物变化,无新浏览器/覆盖率结论。
见[契约](baselines/thumbnail-contract.md)、[验证](baselines/thumbnail-contract-validation.json)
和[变更](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md)。下一项为抽帧与失败测试。
222项:110 done、10 doing、102 todo;单项本地提交,不推送或发布。
## PKG-IFRAME-05 原生缓存与中断导航检查点(doing)
新增独立 `yarn test:iframe-history`:完整Chromium通道与可缓存HTTP页面,避免默认
+4 -1
View File
@@ -19,7 +19,7 @@
| BASE-TYPE-02 | resolved / 已复现 | chapter 可选运行时参数被声明为必填 | PKG-CHAPTER-02 |
| BASE-TYPE-03 | resolved / 已复现 | chapter legacy 缺旧模块解析的声明回退 | ENG-04, PKG-CHAPTER-04 |
| BASE-TYPE-04 | resolved / 已复现 | plugins.add / toggle 的旧声明返回值与运行时不同 | CORE-07, CORE-08, CORE-10, CORE-21 |
| BASE-DIST-01 | open / 源码/产物事实 | thumbnail tool 分发入口与类型文件缺失 | PKG-TOOL-THUMB-01, PKG-TOOL-THUMB-04 |
| BASE-DIST-01 | open / 已复现 | thumbnail tool 分发入口与类型文件缺失 | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| BASE-PERF-01 | resolved / 已复现 | 销毁后的 resize 防抖重新创建 notice timer | CORE-17, CORE-04, CORE-18 |
| BASE-SOURCE-01 | open / 源码/产物事实 | 工作区 5.4.1 与采集时真实发布 5.4.0 不同 | BASE-08, REL-01 |
| BASE-SOURCE-02 | open / 源码/产物事实 | registry gitHead 与发布包版本对应不可靠 | ENG-07, REL-01 |
@@ -177,3 +177,6 @@
| IFRAME-LIFE-01 | open / 已复现 | Iframe timestamp IDs and unowned pending requests/timers lack terminal cleanup | PKG-IFRAME-02, PKG-IFRAME-03, PKG-IFRAME-05 |
| IFRAME-TRUST-01 | open / 已复现 | Iframe executable commit protocol accepts messages without source/origin validation | PKG-IFRAME-03, PKG-IFRAME-05 |
| IFRAME-DIST-01 | open / 已复现 | Iframe tool name is unavailable at registry observation and old archive includes distinct helper exports | PKG-IFRAME-04, PKG-IFRAME-06 |
| THUMB-COMPAT-01 | open / 已复现 | Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults | PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-04 |
| THUMB-LIFE-01 | open / 已复现 | Thumbnail drop binding and repeated destroy fail; pending work and replacement resource ownership need repair | PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-05 |
| THUMB-PROVENANCE-01 | open / 源码/产物事实 | Thumbnail local emitter attribution and historical distribution completeness remain unverified | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
+61 -5
View File
@@ -360,16 +360,19 @@
{
"id": "BASE-DIST-01",
"title": "thumbnail tool 分发入口与类型文件缺失",
"confirmation": "source-observed",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-TOOL-THUMB-01",
"PKG-TOOL-THUMB-04"
"PKG-TOOL-THUMB-04",
"PKG-TOOL-THUMB-06"
],
"evidence": [
"refactor/baselines/distribution.json"
"refactor/baselines/distribution.json",
"refactor/baselines/thumbnail-contract.md",
"refactor/baselines/thumbnail-release.json",
"refactor/scripts/thumbnail-contract.test.mjs"
],
"compatibleResolution": "核实真实旧 npm 后保留 .esm.js 等历史路径的兼容入口并补声明。",
"compatibleResolution": "Recovered 3.5.31 has main and a CSS entry; baseline workspace lacks main and points to missing .esm.js/types. Recoverable CDN main is verified against Git, not an original tarball. Preserve supported paths, decide historical CSS disposition, add real declarations and verify isolated installs in 04/06.",
"closureCriteria": "隔离打包消费者能加载所有原承诺路径,dist 不作为源码直接手改。"
},
{
@@ -3844,6 +3847,59 @@
"refactor/changes/2026-09-13-PKG-IFRAME-04-types.md",
"refactor/baselines/iframe-types-validation.json"
]
},
{
"id": "THUMB-COMPAT-01",
"title": "Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults",
"confirmation": "reproduced",
"owners": [
"PKG-TOOL-THUMB-02",
"PKG-TOOL-THUMB-03",
"PKG-TOOL-THUMB-04"
],
"compatibleResolution": "Freeze both implementations and derive a concrete compatibility policy before changing defaults; no silent claim that identical calls have identical historical semantics.",
"closureCriteria": "Explicit documented compatibility decision, old/candidate tests and migration examples covering delay, height, input reset and readiness.",
"status": "open",
"evidence": [
"refactor/baselines/thumbnail-contract.md",
"refactor/baselines/thumbnail-release.json",
"refactor/scripts/thumbnail-contract.test.mjs"
]
},
{
"id": "THUMB-LIFE-01",
"title": "Thumbnail drop binding and repeated destroy fail; pending work and replacement resource ownership need repair",
"confirmation": "reproduced",
"owners": [
"PKG-TOOL-THUMB-02",
"PKG-TOOL-THUMB-03",
"PKG-TOOL-THUMB-05"
],
"compatibleResolution": "Drop and duplicate-destroy defects are reproduced; reproduce source-observed extraction/timer/blob/input ownership gaps before fixes, then introduce owned cleanup without changing public shape.",
"closureCriteria": "Deterministic old failure/candidate success for replacement, error and destruction plus real media/browser resource validation.",
"status": "open",
"evidence": [
"refactor/baselines/thumbnail-contract.md",
"refactor/baselines/thumbnail-release.json",
"refactor/scripts/thumbnail-contract.test.mjs"
]
},
{
"id": "THUMB-PROVENANCE-01",
"title": "Thumbnail local emitter attribution and historical distribution completeness remain unverified",
"confirmation": "source-observed",
"owners": [
"PKG-TOOL-THUMB-04",
"PKG-TOOL-THUMB-06"
],
"compatibleResolution": "Historical release declares tiny-emitter; review local emitter provenance/license before TS migration. Keep recovered individual CDN files distinct from full npm archive evidence.",
"closureCriteria": "Reviewed emitter attribution and license ledger plus explicit historical artifact recovery limits and complete candidate tarball evidence.",
"status": "open",
"evidence": [
"refactor/baselines/thumbnail-contract.md",
"refactor/baselines/thumbnail-release.json",
"refactor/scripts/thumbnail-contract.test.mjs"
]
}
]
}
+53
View File
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { hash, refactorDir } from './releases.mjs'
export function verifyThumbnailContract() {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/thumbnail-release.json')))
assert.equal(baseline.recovered.originalTarballAvailable, false)
assert.equal(baseline.recovered.completeArchiveAvailable, false)
assert.equal(new Set(baseline.observations.map(item => item.url)).size, baseline.observations.length)
const source = new Map()
const historical = new Map()
for (const [commit, entries, target] of [[baseline.sourceCommit, baseline.source, source], [baseline.recovered.historicalCommit, baseline.historicalSource, historical]]) {
for (const [file, expected] of Object.entries(entries)) {
const text = execFileSync('git', ['show', `${commit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(text), expected, file)
target.set(file, text)
}
}
for (const item of baseline.observations) {
const bytes = item.identicalGit
? execFileSync('git', ['show', `${item.identicalGit.commit}:${item.identicalGit.file}`])
: Buffer.from(item.text)
assert.equal(hash(bytes), item.sha256, item.url)
assert.equal(bytes.length, item.bytes, item.url)
}
const registry = baseline.observations.find(item => item.url === 'https://registry.npmjs.org/artplayer-tool-thumbnail')
assert.equal(registry.status, 200)
const metadata = JSON.parse(registry.text)
assert.equal(metadata.name, baseline.recovered.name)
assert(metadata.time.unpublished.versions.includes('3.5.31'))
assert.equal(metadata.versions, undefined)
const manifest = baseline.observations.find(item => item.url.endsWith('@3.5.31/package.json'))
assert.equal(manifest.status, 200)
assert.deepEqual(JSON.parse(manifest.text), baseline.recovered.manifest)
assert.equal(baseline.recovered.manifest.gitHead, baseline.recovered.historicalCommit)
const oldManifest = JSON.parse(historical.get('packages/artplayer-tool-thumbnail/package.json'))
assert.deepEqual({ ...oldManifest, gitHead: null }, { ...baseline.recovered.manifest, gitHead: null })
for (const [name, association] of Object.entries(baseline.coreAssociations)) {
const files = name === 'historical' ? historical : source
assert.equal(JSON.parse(files.get(association.file)).version, association.version)
}
return { baseline, source, historical }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const { baseline } = verifyThumbnailContract()
console.log(`Thumbnail contract: ${baseline.observations.length} frozen observations, ${Object.keys(baseline.source).length} workspace inputs, ${Object.keys(baseline.historicalSource).length} historical Git inputs; recovered CDN main is byte-identical, original npm archive remains unavailable`)
}
@@ -0,0 +1,200 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
// eslint-disable-next-line test/no-import-node-test -- Frozen package contract runner.
import test from 'node:test'
import { thumbnailEnvironment, thumbnailHistorical } from '../../test/helpers/thumbnail.js'
import { verifyThumbnailContract } from './thumbnail-contract.mjs'
const plain = value => JSON.parse(JSON.stringify(value))
const root = 'packages/artplayer-tool-thumbnail/'
test('Thumbnail recovered CDN files are not represented as a verified npm archive', () => {
const { baseline, source, historical } = verifyThumbnailContract()
assert.equal(baseline.observations.length, 11)
assert.equal(source.size, 12)
assert.equal(historical.size, 8)
assert.equal(baseline.recovered.originalTarballAvailable, false)
assert.equal(baseline.recovered.completeArchiveAvailable, false)
const old = baseline.recovered.manifest
const current = JSON.parse(source.get(`${root}package.json`))
assert.equal(old.main, 'dist/artplayer-tool-thumbnail.js')
assert.equal(old.style, 'dist/artplayer-tool-thumbnail.css')
assert.equal(old.dependencies['tiny-emitter'], '^2.1.0')
assert.equal(current.main, undefined)
assert.equal(current.module, 'dist/artplayer-tool-thumbnail.esm.js')
assert.equal(current.types, 'types/artplayer-tool-thumbnail.d.ts')
assert.equal(source.has(`${root}${current.module}`), false)
assert.equal(source.has(`${root}${current.types}`), false)
const tree = execFileSync('git', ['ls-tree', '-r', '--name-only', baseline.sourceCommit, root], { encoding: 'utf8' }).split('\n')
assert.equal(tree.includes(`${root}${current.module}`), false)
assert.equal(tree.includes(`${root}${current.types}`), false)
assert.match(source.get('docs/assets/example/tool.thumbnail.js'), /new ArtplayerToolThumbnail/)
assert.match(source.get('docs/assets/example/thumbnail.js'), /artplayerPluginThumbnail/)
})
test('Thumbnail frozen workspace ESM exports only the default constructor', async () => {
const { source } = verifyThumbnailContract()
const esm = await import(`data:text/javascript,${encodeURIComponent(source.get(`${root}dist/artplayer-tool-thumbnail.mjs`))}`)
assert.deepEqual(Object.keys(esm), ['default'])
assert.equal(typeof esm.default, 'function')
})
for (const implementation of thumbnailHistorical()) {
const create = (options = {}) => {
const env = thumbnailEnvironment(implementation)
const instance = new env.Factory({ fileInput: new env.Element('input'), ...options })
return { ...env, instance }
}
test(`Thumbnail ${implementation.name}: direct CJS and script class retain public spellings and fields`, () => {
const env = create()
assert.equal(typeof env.exported, 'function')
assert.equal(env.exported.default, undefined)
assert.equal(typeof thumbnailEnvironment(implementation, true).exported, 'function')
assert.deepEqual(Object.getOwnPropertyNames(env.Factory.prototype), ['constructor', 'ondrop', 'setup', 'inputChange', 'loadVideo', 'start', 'creatScreenshotDate', 'creatCanvas', 'download', 'errorHandle', 'destroy'])
assert.deepEqual(Object.keys(env.instance), ['processing', 'option', 'video', 'duration', 'inputChange', 'ondrop'])
assert.equal(typeof env.Factory.creatVideo, 'function')
assert.equal(typeof env.Factory.ondragover, 'function')
assert.equal(env.Factory.ondrop, undefined)
assert.equal(env.instance.video.muted, true)
assert.equal(env.instance.video.controls, true)
assert.equal(env.instance.video.parentNode, env.body)
assert.equal(env.instance.processing, false)
assert.equal(env.instance.duration, 0)
})
test(`Thumbnail ${implementation.name}: fresh defaults, setup clamps, unknown options and wrapper`, () => {
const env = create({ number: 1, width: 2000, column: 0, custom: 123 })
assert.notEqual(env.Factory.DEFAULTS, env.Factory.DEFAULTS)
assert.deepEqual(plain(env.Factory.DEFAULTS), { ...(implementation.legacy ? { delay: 300 } : {}), number: 60, width: 160, height: 90, column: 10, begin: 0, end: null })
assert(Number.isNaN(env.Factory.DEFAULTS.end))
assert.equal(env.instance.option.number, 10)
assert.equal(env.instance.option.width, 1000)
assert.equal(env.instance.option.column, 1)
assert.equal(env.instance.option.custom, 123)
assert.equal(env.instance.setup({ number: 12.5 }), env.instance)
assert.equal(env.instance.option.number, 12.5)
assert.throws(() => env.instance.setup({ width: '160' }), /The 'width' is not a number/)
assert.throws(() => new env.Factory({ fileInput: {} }), /The 'fileInput' is not a Element/)
const wrapper = new env.Element('div')
const wrapped = new env.Factory({ fileInput: wrapper })
assert.equal(wrapper.style.position, 'relative')
assert.equal(wrapped.option.fileInput, wrapper.children[0])
assert.equal(wrapped.option.fileInput.style.opacity, '0')
assert.equal(wrapped.option.fileInput.type, 'file')
})
test(`Thumbnail ${implementation.name}: event snapshot, once identity, context and chaining`, () => {
const { instance } = create()
const seen = []
const ctx = { marker: 1 }
function second(value) {
seen.push([this, value])
}
assert.equal(instance.on('value', () => instance.off('value', second)), instance)
assert.equal(instance.once('value', second, ctx), instance)
assert.equal(instance.emit('value', 7), instance)
instance.emit('value', 8)
assert.deepEqual(seen, [[ctx, 7]])
instance.once('removed', second, ctx)
assert.equal(instance.off('removed', second), instance)
instance.emit('removed', 9)
assert.equal(seen.length, 1)
assert.equal(instance.off('value'), instance)
})
test(`Thumbnail ${implementation.name}: file and video event timing identifies old delay semantics`, async () => {
const env = create()
const file = { name: 'video.mp4', type: 'video/mp4' }
const seen = []
env.instance.on('file', value => seen.push(['file', value, env.instance.video.src]))
env.instance.on('video', value => seen.push(['video', value, value.src]))
assert.equal(env.instance.loadVideo(file), undefined)
assert.equal(env.instance.file, file)
assert.equal(env.instance.videoUrl, 'blob:thumbnail-1')
assert.deepEqual(seen[0], ['file', file, undefined])
assert.equal(seen.length, implementation.legacy ? 1 : 2)
assert.equal(env.timers.size, implementation.legacy ? 1 : 0)
if (implementation.legacy) {
const [id, timer] = [...env.timers][0]
assert.equal(timer.delay, 300)
env.timers.delete(id)
timer.callback()
await Promise.resolve()
}
assert.deepEqual(seen[1], ['video', env.instance.video, 'blob:thumbnail-1'])
assert.equal(env.instance.loadVideo(undefined), undefined)
assert.equal(seen.length, 2)
})
test(`Thumbnail ${implementation.name}: input reset and existing ineffective drop binding stay distinguished`, () => {
const env = create()
const input = env.instance.option.fileInput
input.value = 'selected.mp4'
input.files = [{ name: 'selected.mp4', type: 'video/mp4' }]
assert.equal(input.handlers.get('change').has(env.instance.inputChange), true)
assert.equal(input.handlers.has('drop'), false)
env.instance.inputChange({ target: input })
assert.equal(input.value, implementation.legacy ? 'selected.mp4' : '')
let prevented = 0
const preventDefault = () => {
prevented++
}
env.instance.ondrop({ preventDefault, dataTransfer: { files: [] } })
env.Factory.ondragover({ preventDefault })
assert.equal(prevented, 2)
})
test(`Thumbnail ${implementation.name}: midpoint grid and footer geometry`, () => {
const env = create({ number: 10, width: 20, height: 30, column: 3, begin: 5 })
env.instance.duration = 100
assert.deepEqual(plain(env.instance.creatScreenshotDate()), Array.from({ length: 10 }, (_, i) => ({ time: 10 + i * 10, x: i % 3 * 20, y: Math.floor(i / 3) * 30 })))
const canvas = env.instance.creatCanvas()
assert.equal(canvas.width, 60)
assert.equal(canvas.height, 150)
assert.deepEqual(env.operations.filter(item => item.name === 'fillRect' || item.name === 'fillText'), [
{ name: 'fillRect', args: [0, 0, 60, 150] },
{ name: 'fillText', args: ['From: https://artplayer.org/, Number: 10, Width: 20, Height: 30, Column: 3', 10, 139] },
])
assert.equal(env.context2D.font, '14px Georgia')
})
test(`Thumbnail ${implementation.name}: configured height versus source aspect ratio predates this refactor`, () => {
const { instance } = create({ height: 25, width: 160 })
instance.video.duration = 1
instance.file = { name: 'short.mp4' }
assert.throws(() => instance.start(), /preview density cannot be greater than 1/)
assert.equal(instance.option.height, implementation.legacy ? 25 : 90)
assert.equal(instance.processing, false)
})
test(`Thumbnail ${implementation.name}: synchronous errors, download names and destroy cleanup contract`, () => {
const env = create()
const errors = []
env.instance.on('error', message => errors.push(message))
assert.throws(() => env.instance.download(), /Download does not seem to be ready/)
assert.equal(errors.length, 1)
assert.equal(env.instance.errorHandle(true, 'unused'), undefined)
env.instance.video.canPlayType = () => ''
assert.throws(() => env.instance.loadVideo({ type: 'bad/type' }), /Playback of this file format is not supported: bad\/type/)
assert.equal(errors.length, 2)
env.instance.videoUrl = 'blob:video'
env.instance.thumbnailUrl = 'blob:sheet'
const downloads = []
env.instance.on('download', name => downloads.push(name))
for (const name of ['movie.part.mp4', 'noextension']) {
env.instance.file = { name }
assert.equal(env.instance.download(), env.instance)
}
assert.deepEqual(downloads, ['movie.part.png', '.png'])
assert.deepEqual(env.operations.filter(item => item.name === 'click').map(item => item.download), downloads)
let destroyed = 0
env.instance.on('destroy', () => destroyed++)
assert.equal(env.instance.destroy(), undefined)
assert.equal(destroyed, 1)
assert.equal(env.body.children.length, 0)
assert.equal(env.instance.option.fileInput.handlers.get('change').size, 0)
assert.deepEqual(env.operations.filter(item => item.name === 'revokeURL').map(item => item.url), ['blob:video', 'blob:sheet'])
assert.throws(() => env.instance.destroy(), /NotFoundError/)
})
}
+8 -3
View File
@@ -3742,11 +3742,16 @@
"dependsOn": [
"BASE-05"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "构造/defaults/事件/方法/历史拼写、.esm.js 入口和缺失 types",
"acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;核实 BASE-DIST-01 的 .esm.js 旧入口、缺失声明及无 main 的历史发布事实",
"evidence": []
"acceptance": "源码/声明/README/demo/可恢复发布内容差异已登记;公开形状和版本范围冻结;区分旧 npm 的 main/CSS 与当前工作区无 main、.esm.js/类型缺失,不把工作区状态冒称旧 npm 发布事实",
"evidence": [
"baselines/thumbnail-release.json",
"baselines/thumbnail-contract.md",
"changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md",
"baselines/thumbnail-contract-validation.json"
]
},
{
"id": "PKG-TOOL-THUMB-02",
+95
View File
@@ -0,0 +1,95 @@
import vm from 'node:vm'
import { verifyThumbnailContract } from '../../refactor/scripts/thumbnail-contract.mjs'
export function thumbnailHistorical() {
const { source, historical } = verifyThumbnailContract()
return [
{ name: 'cdn-3.5.31', code: historical.get('packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js'), legacy: true },
...['.js', '.legacy.js'].map(suffix => ({ name: `workspace${suffix}`, code: source.get(`packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail${suffix}`), legacy: false })),
]
}
export function thumbnailEnvironment(implementation, script = false) {
const operations = []
const timers = new Map()
let nextTimer = 0
let nextUrl = 0
const context2D = Object.fromEntries(['fillRect', 'fillText', 'drawImage'].map(name => [name, (...args) => operations.push({ name, args })]))
class Element {
constructor(tagName = 'div') {
this.tagName = tagName.toUpperCase()
this.type = this.tagName === 'INPUT' ? 'file' : ''
this.style = {}
this.children = []
this.handlers = new Map()
this.files = []
if (this.tagName === 'VIDEO') {
this.duration = 0
this.videoWidth = 160
this.videoHeight = 90
this.currentTime = 0
this.canPlayType = () => 'probably'
}
}
appendChild(child) {
child.parentNode = this
this.children.push(child)
operations.push({ name: 'append', parent: this.tagName, child: child.tagName })
return child
}
removeChild(child) {
const index = this.children.indexOf(child)
if (index === -1)
throw new Error('NotFoundError')
this.children.splice(index, 1)
child.parentNode = null
return child
}
addEventListener(name, callback) {
operations.push({ name: 'listen', type: name, callback })
if (!callback)
return
const list = this.handlers.get(name) || new Set()
list.add(callback)
this.handlers.set(name, list)
}
removeEventListener(name, callback) { this.handlers.get(name)?.delete(callback) }
getContext() { return context2D }
click() { operations.push({ name: 'click', download: this.download, href: this.href }) }
toBlob(callback) { callback({ kind: 'image/png' }) }
}
const body = new Element('body')
const module = { exports: {} }
const box = {
Element,
document: { body, createElement: tag => new Element(tag) },
URL: {
createObjectURL(value) {
const url = `blob:thumbnail-${++nextUrl}`
operations.push({ name: 'createURL', value, url })
return url
},
revokeObjectURL(url) { operations.push({ name: 'revokeURL', url }) },
},
setTimeout(callback, delay) {
const id = ++nextTimer
timers.set(id, { callback, delay })
return id
},
clearTimeout(id) { timers.delete(id) },
}
box.window = box
box.self = box
if (!script) {
box.module = module
box.exports = module.exports
}
vm.runInNewContext(implementation.code, box)
const exported = script ? box.ArtplayerToolThumbnail : module.exports
const Factory = exported.default || exported
return { box, Element, body, Factory, exported, operations, context2D, timers }
}