mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(thumbnail): [PKG-TOOL-THUMB-01] freeze recovered release and workspace contracts
This commit is contained in:
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."
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
@@ -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)
|
||||
@@ -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页面,避免默认
|
||||
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user