修复资源画本转场快照丢失卡片预览图
快照克隆原先直接复制 blob: 对象 URL,源卡片卸载或预览缓存驱逐后该 URL 被 revoke,克隆里的图片随即失效,表现为进入/退出子画布时卡片框在动而图片不跟着展开和复原 对 blob: 来源且已解码的图片在克隆时栅格化为 data URL 冻结,与既有视频快照处理保持一致 补充 resourceBookController 快照图片冻结回归用例
This commit is contained in:
@@ -23,6 +23,26 @@ function validRect(rect: Snapshot['rect']) {
|
||||
);
|
||||
}
|
||||
|
||||
// 卡片预览用 object URL 渲染;源节点卸载或预览缓存驱逐会 revoke 掉它。
|
||||
// 快照必须自带像素,否则展开/复原过程中图片会直接消失。
|
||||
function freezeBlobImageSource(image: HTMLImageElement) {
|
||||
const source = image.currentSrc || image.getAttribute('src') || '';
|
||||
if (!source.startsWith('blob:')) return null;
|
||||
if (!image.complete || image.naturalWidth <= 0 || image.naturalHeight <= 0)
|
||||
return null;
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return null;
|
||||
context.drawImage(image, 0, 0);
|
||||
return canvas.toDataURL();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Freeze presentation only. Clones cannot expose duplicate controls, replay
|
||||
// media, or depend on CSS selectors belonging to the old ancestor tree.
|
||||
function clonePresentation(
|
||||
@@ -74,6 +94,10 @@ function clonePresentation(
|
||||
copy.style.animation = 'none';
|
||||
copy.style.pointerEvents = 'none';
|
||||
copy.setAttribute('tabindex', '-1');
|
||||
if (original instanceof HTMLImageElement) {
|
||||
const frozen = freezeBlobImageSource(original);
|
||||
if (frozen) copy.setAttribute('src', frozen);
|
||||
}
|
||||
if (copy instanceof HTMLMediaElement) {
|
||||
copy.removeAttribute('src');
|
||||
copy.querySelectorAll('source').forEach((item) => item.remove());
|
||||
|
||||
@@ -23,7 +23,7 @@ function rect(left = 0, top = 0, width = 100, height = 80): DOMRect {
|
||||
function fixture() {
|
||||
const root = document.createElement('div');
|
||||
root.innerHTML = `<div class="game-resource-book-scene"><div class="game-resource-book-scene-world">
|
||||
<div class="game-resource-card" data-resource-card-id="one"><button id="original">Open</button></div>
|
||||
<div class="game-resource-card" data-resource-card-id="one"><img src="blob:card-preview" alt="" /><button id="original">Open</button></div>
|
||||
</div></div><div class="game-resource-book-transition-layer" inert aria-hidden="true"></div>`;
|
||||
document.body.append(root);
|
||||
vi.spyOn(root, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 800, 600));
|
||||
@@ -100,6 +100,44 @@ describe('resource book FLIP controller', () => {
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('freezes blob-backed card previews into the snapshot', () => {
|
||||
const { root, measure } = setup();
|
||||
const image = root.querySelector<HTMLImageElement>(
|
||||
'.game-resource-card img',
|
||||
)!;
|
||||
Object.defineProperty(image, 'complete', {
|
||||
configurable: true,
|
||||
value: true,
|
||||
});
|
||||
Object.defineProperty(image, 'naturalWidth', {
|
||||
configurable: true,
|
||||
value: 8,
|
||||
});
|
||||
Object.defineProperty(image, 'naturalHeight', {
|
||||
configurable: true,
|
||||
value: 6,
|
||||
});
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
||||
drawImage: vi.fn(),
|
||||
} as unknown as CanvasRenderingContext2D);
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue(
|
||||
'data:image/png;base64,FROZEN',
|
||||
);
|
||||
|
||||
const controller = createResourceBookTransitionController();
|
||||
const token = controller.begin(root);
|
||||
measure.mockReturnValue(rect(150, 100, 200, 160));
|
||||
controller.play(root, token, () => undefined);
|
||||
|
||||
const snapshotImage = root.querySelector<HTMLImageElement>(
|
||||
'.game-resource-book-transition-layer img',
|
||||
);
|
||||
expect(snapshotImage?.getAttribute('src')).toBe(
|
||||
'data:image/png;base64,FROZEN',
|
||||
);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('retargets from the current overlay geometry and ignores old completion', async () => {
|
||||
const { root, measure, flights } = setup();
|
||||
const controller = createResourceBookTransitionController();
|
||||
|
||||
Reference in New Issue
Block a user