From 4cfbcb60d6fdff0c3a976628e64c1df325446847 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Mon, 15 Jun 2026 10:56:42 +0800 Subject: [PATCH 1/6] fix(loader): prefer virtualPathResourceMap type over URL extension inference When loading an asset by URL, prefer the type registered in _virtualPathResourceMap over inferring from the URL file extension. This ensures editor-registered assets with non-standard extensions (e.g. virtual paths without extensions) resolve to the correct loader. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/asset/ResourceManager.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 0395b2b29b..94cb7ee946 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -340,7 +340,12 @@ export class ResourceManager { } private _assignDefaultOptions(assetInfo: LoadItem): LoadItem { - assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + const remoteConfig = this._virtualPathResourceMap[assetInfo.url]; + if (remoteConfig) { + assetInfo.type = remoteConfig.type; + } else { + assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + } if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } From 66d7929aa47247b44b968254df078976ad5c8717 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 16 Jun 2026 13:52:25 +0800 Subject: [PATCH 2/6] fix(core): correct virtualPath type resolution in _assignDefaultOptions Strip the query (?q=) before looking up `_virtualPathResourceMap`, so a sub-asset virtual path resolves its type the same way `_loadSingleItem` resolves its path via `assetBaseURL`. Keep explicit `type` taking precedence over the map (the map only overrides URL-extension inference, matching the PR title), and guard against an undefined `url` for urls-only loads such as TextureCube. Add ResourceManager tests covering extensionless virtual path, sub-asset query path, and explicit-type precedence. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/asset/ResourceManager.ts | 9 ++---- .../src/core/resource/ResourceManager.test.ts | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 94cb7ee946..6a12861aa0 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -340,12 +340,9 @@ export class ResourceManager { } private _assignDefaultOptions(assetInfo: LoadItem): LoadItem { - const remoteConfig = this._virtualPathResourceMap[assetInfo.url]; - if (remoteConfig) { - assetInfo.type = remoteConfig.type; - } else { - assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); - } + const assetBaseURL = assetInfo.url?.split("?")[0]; + const remoteType = assetBaseURL ? this._virtualPathResourceMap[assetBaseURL]?.type : undefined; + assetInfo.type = assetInfo.type ?? remoteType ?? ResourceManager._getTypeByUrl(assetInfo.url); if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index 61c62d7319..db14700a8a 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -87,4 +87,36 @@ describe("ResourceManager", () => { } }); }); + + describe("assignDefaultOptions virtualPath type", () => { + it("infers type from virtualPathResourceMap for extensionless url", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/extensionless", path: "https://cdn.ali.com/a.json", type: "Texture2D" } + ]); + // @ts-ignore + const item = resourceManager._assignDefaultOptions({ url: "Assets/extensionless" }); + expect(item.type).equal("Texture2D"); + }); + + it("infers type for sub-asset query path", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/withSubAsset", path: "https://cdn.ali.com/b.glb", type: "GLTF" } + ]); + // @ts-ignore + const item = resourceManager._assignDefaultOptions({ url: "Assets/withSubAsset?q=materials[0]" }); + expect(item.type).equal("GLTF"); + }); + + it("keeps explicit type over virtualPathResourceMap type", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/explicit", path: "https://cdn.ali.com/c.bin", type: "GLTF" } + ]); + // @ts-ignore + const item = resourceManager._assignDefaultOptions({ url: "Assets/explicit", type: "Texture2D" }); + expect(item.type).equal("Texture2D"); + }); + }); }); From e78e08bbe87a23e7151f2b89eaaf9c37a5fdeba1 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 16 Jun 2026 15:49:02 +0800 Subject: [PATCH 3/6] fix(core): unify virtualPath type/path resolution and fix sub-asset loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve asset type and remote path from a single _parseURL + virtualPath lookup, and move baseUrl resolution after the lookup so virtual paths are no longer polluted by baseUrl. _assignDefaultOptions now takes the resolved remoteConfig instead of querying the map itself. - Merge `urls` into `url` before _parseURL — fixes urls-only (TextureCube) throw - Always strip the `?q=` query from item.url so the sub-asset query no longer leaks into the cache key / loadingPromise dedup / request url - Move virtualPath type checks to the public load() layer (drop tests that poked the private _assignDefaultOptions); sync the _parseURL field rename in the existing queryPath tests Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/asset/ResourceManager.ts | 48 +++++++---------- .../src/core/resource/ResourceManager.test.ts | 51 +++++++++---------- 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 6a12861aa0..b2516b3007 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -339,34 +339,31 @@ export class ResourceManager { this._loadingPromises = null; } - private _assignDefaultOptions(assetInfo: LoadItem): LoadItem { - const assetBaseURL = assetInfo.url?.split("?")[0]; - const remoteType = assetBaseURL ? this._virtualPathResourceMap[assetBaseURL]?.type : undefined; - assetInfo.type = assetInfo.type ?? remoteType ?? ResourceManager._getTypeByUrl(assetInfo.url); + private _assignDefaultOptions(assetInfo: LoadItem, remoteConfig?: EditorResourceItem): LoadItem { + assetInfo.type ||= remoteConfig?.type ?? ResourceManager._getTypeByUrl(assetInfo.url); if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } assetInfo.retryCount = assetInfo.retryCount ?? this.retryCount; assetInfo.timeout = assetInfo.timeout ?? this.timeout; assetInfo.retryInterval = assetInfo.retryInterval ?? this.retryInterval; - assetInfo.url = assetInfo.url ?? assetInfo.urls.join(","); return assetInfo; } private _loadSingleItem(itemOrURL: LoadItem | string): AssetPromise { - const item = this._assignDefaultOptions(typeof itemOrURL === "string" ? { url: itemOrURL } : itemOrURL); - let { url } = item; - - // Not absolute and base url is set - if (!Utils.isAbsoluteUrl(url) && this.baseUrl) url = Utils.resolveAbsoluteUrl(this.baseUrl, url); - + const item = typeof itemOrURL === "string" ? { url: itemOrURL } : itemOrURL; + item.url = item.url ?? item.urls.join(","); // Parse url - const { assetBaseURL, queryPath } = this._parseURL(url); + const { url, queryPath } = this._parseURL(item.url); const paths = queryPath ? this._parseQueryPath(queryPath) : []; // Get remote asset base url - const remoteConfig = this._virtualPathResourceMap[assetBaseURL]; - const remoteAssetBaseURL = remoteConfig?.path ?? assetBaseURL; + const remoteConfig = this._virtualPathResourceMap[url]; + this._assignDefaultOptions(item, remoteConfig); + + // Not absolute and base url is set + item.url = !Utils.isAbsoluteUrl(url) && this.baseUrl ? Utils.resolveAbsoluteUrl(this.baseUrl, url) : url; + const remoteAssetBaseURL = remoteConfig?.path ?? item.url; // Check cache const cacheObject = this._assetUrlPool[remoteAssetBaseURL]; @@ -415,7 +412,7 @@ export class ResourceManager { // Check whether load main asset const mainPromise = loadingPromises[remoteAssetBaseURL] || - this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, assetBaseURL, subpackageName); + this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, subpackageName); mainPromise.catch((e) => { this._onSubAssetFail(remoteAssetBaseURL, queryPath, e); }); @@ -423,7 +420,7 @@ export class ResourceManager { return this._createSubAssetPromiseCallback(remoteAssetBaseURL, remoteAssetURL, queryPath); } - return this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, assetBaseURL, subpackageName); + return this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, subpackageName); } // For adapter mini-game platform @@ -431,19 +428,12 @@ export class ResourceManager { loader: Loader, item: LoadItem, remoteAssetBaseURL: string, - assetBaseURL: string, subpackageName: string ): AssetPromise { - return this._loadMainAsset(loader, item, remoteAssetBaseURL, assetBaseURL); + return this._loadMainAsset(loader, item, remoteAssetBaseURL); } - private _loadMainAsset( - loader: Loader, - item: LoadItem, - remoteAssetBaseURL: string, - assetBaseURL: string - ): AssetPromise { - item.url = assetBaseURL; + private _loadMainAsset(loader: Loader, item: LoadItem, remoteAssetBaseURL: string): AssetPromise { const loadingPromises = this._loadingPromises; const promise = loader.load(item, this); loadingPromises[remoteAssetBaseURL] = promise; @@ -527,10 +517,10 @@ export class ResourceManager { return subResource; } - private _parseURL(path: string): { assetBaseURL: string; queryPath: string } { + private _parseURL(path: string): { url: string; queryPath: string } { const [baseUrl, searchStr] = path.split("?"); let queryPath = undefined; - let assetBaseURL = baseUrl; + let url = baseUrl; if (searchStr) { const params = searchStr.split("&"); for (let i = params.length - 1; i >= 0; i--) { @@ -541,9 +531,9 @@ export class ResourceManager { break; } } - assetBaseURL = params.length > 0 ? baseUrl + "?" + params.join("&") : baseUrl; + url = params.length > 0 ? baseUrl + "?" + params.join("&") : baseUrl; } - return { assetBaseURL, queryPath }; + return { url, queryPath }; } private _parseQueryPath(string): string[] { diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index db14700a8a..d0c0933e38 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -1,4 +1,4 @@ -import { AssetType, ResourceManager, Texture2D } from "@galacean/engine"; +import { AssetPromise, AssetType, ResourceManager, Texture2D } from "@galacean/engine"; import "@galacean/engine-loader"; import { WebGLEngine } from "@galacean/engine"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -40,24 +40,24 @@ describe("ResourceManager", () => { describe("queryPath", () => { it("no encode", () => { // @ts-ignore - const { assetBaseURL } = engine.resourceManager._parseURL( + const { url } = engine.resourceManager._parseURL( "https://cdn.ali.com/inner.jpg?x-oss-process=image/resize,l_1024" ); - expect(assetBaseURL).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image/resize,l_1024"); + expect(url).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image/resize,l_1024"); }); it("encode", () => { // @ts-ignore - const { assetBaseURL } = engine.resourceManager._parseURL( + const { url } = engine.resourceManager._parseURL( "https://cdn.ali.com/inner.jpg?x-oss-process=image%25resize,l_1024" ); - expect(assetBaseURL).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image%25resize,l_1024"); + expect(url).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image%25resize,l_1024"); }); it("query path", () => { // @ts-ignore - const { assetBaseURL, queryPath } = engine.resourceManager._parseURL("https://cdn.ali.com/inner.jpg?q=abc"); - expect(assetBaseURL).equal("https://cdn.ali.com/inner.jpg"); + const { url, queryPath } = engine.resourceManager._parseURL("https://cdn.ali.com/inner.jpg?q=abc"); + expect(url).equal("https://cdn.ali.com/inner.jpg"); expect(queryPath).equal("abc"); }); }); @@ -88,35 +88,34 @@ describe("ResourceManager", () => { }); }); - describe("assignDefaultOptions virtualPath type", () => { - it("infers type from virtualPathResourceMap for extensionless url", () => { + describe("virtualPath loading", () => { + it("infers loader type from virtualPathResourceMap when type is omitted", () => { const resourceManager = engine.resourceManager; resourceManager.initVirtualResources([ { virtualPath: "Assets/extensionless", path: "https://cdn.ali.com/a.json", type: "Texture2D" } ]); // @ts-ignore - const item = resourceManager._assignDefaultOptions({ url: "Assets/extensionless" }); - expect(item.type).equal("Texture2D"); - }); + const loaderSpy = vi + .spyOn(ResourceManager._loaders["Texture2D"], "load") + .mockReturnValue(new AssetPromise(() => {})); - it("infers type for sub-asset query path", () => { - const resourceManager = engine.resourceManager; - resourceManager.initVirtualResources([ - { virtualPath: "Assets/withSubAsset", path: "https://cdn.ali.com/b.glb", type: "GLTF" } - ]); - // @ts-ignore - const item = resourceManager._assignDefaultOptions({ url: "Assets/withSubAsset?q=materials[0]" }); - expect(item.type).equal("GLTF"); + resourceManager.load({ url: "Assets/extensionless" }); + + expect(loaderSpy).toHaveBeenCalled(); + expect(loaderSpy.mock.calls[0][0].type).equal("Texture2D"); + loaderSpy.mockRestore(); }); - it("keeps explicit type over virtualPathResourceMap type", () => { + it("shares the main asset across sub-asset queries", () => { const resourceManager = engine.resourceManager; - resourceManager.initVirtualResources([ - { virtualPath: "Assets/explicit", path: "https://cdn.ali.com/c.bin", type: "GLTF" } - ]); // @ts-ignore - const item = resourceManager._assignDefaultOptions({ url: "Assets/explicit", type: "Texture2D" }); - expect(item.type).equal("Texture2D"); + const loaderSpy = vi.spyOn(ResourceManager._loaders["GLTF"], "load").mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load("https://cdn.ali.com/shared.glb?q=materials[0]"); + resourceManager.load("https://cdn.ali.com/shared.glb?q=materials[1]"); + + expect(loaderSpy).toHaveBeenCalledTimes(1); + loaderSpy.mockRestore(); }); }); }); From 046c62e54acff5606bffb9b9d2ee44dc2472c324 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 16 Jun 2026 16:00:02 +0800 Subject: [PATCH 4/6] refactor(core): rename _parseURL field to assetBaseURL, drop unused return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _parseURL returns { assetBaseURL } again — clearer than `url`, which collided with the `item.url` reassigned in _loadSingleItem - _assignDefaultOptions returns void; it mutates in place and the return value was no longer used after the lookup moved to the caller Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/asset/ResourceManager.ts | 20 ++++++++++--------- .../src/core/resource/ResourceManager.test.ts | 12 +++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index b2516b3007..5e5dd61c45 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -339,7 +339,7 @@ export class ResourceManager { this._loadingPromises = null; } - private _assignDefaultOptions(assetInfo: LoadItem, remoteConfig?: EditorResourceItem): LoadItem { + private _assignDefaultOptions(assetInfo: LoadItem, remoteConfig?: EditorResourceItem): void { assetInfo.type ||= remoteConfig?.type ?? ResourceManager._getTypeByUrl(assetInfo.url); if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; @@ -347,22 +347,24 @@ export class ResourceManager { assetInfo.retryCount = assetInfo.retryCount ?? this.retryCount; assetInfo.timeout = assetInfo.timeout ?? this.timeout; assetInfo.retryInterval = assetInfo.retryInterval ?? this.retryInterval; - return assetInfo; } private _loadSingleItem(itemOrURL: LoadItem | string): AssetPromise { const item = typeof itemOrURL === "string" ? { url: itemOrURL } : itemOrURL; item.url = item.url ?? item.urls.join(","); // Parse url - const { url, queryPath } = this._parseURL(item.url); + const { assetBaseURL, queryPath } = this._parseURL(item.url); const paths = queryPath ? this._parseQueryPath(queryPath) : []; // Get remote asset base url - const remoteConfig = this._virtualPathResourceMap[url]; + const remoteConfig = this._virtualPathResourceMap[assetBaseURL]; this._assignDefaultOptions(item, remoteConfig); // Not absolute and base url is set - item.url = !Utils.isAbsoluteUrl(url) && this.baseUrl ? Utils.resolveAbsoluteUrl(this.baseUrl, url) : url; + item.url = + !Utils.isAbsoluteUrl(assetBaseURL) && this.baseUrl + ? Utils.resolveAbsoluteUrl(this.baseUrl, assetBaseURL) + : assetBaseURL; const remoteAssetBaseURL = remoteConfig?.path ?? item.url; // Check cache @@ -517,10 +519,10 @@ export class ResourceManager { return subResource; } - private _parseURL(path: string): { url: string; queryPath: string } { + private _parseURL(path: string): { assetBaseURL: string; queryPath: string } { const [baseUrl, searchStr] = path.split("?"); let queryPath = undefined; - let url = baseUrl; + let assetBaseURL = baseUrl; if (searchStr) { const params = searchStr.split("&"); for (let i = params.length - 1; i >= 0; i--) { @@ -531,9 +533,9 @@ export class ResourceManager { break; } } - url = params.length > 0 ? baseUrl + "?" + params.join("&") : baseUrl; + assetBaseURL = params.length > 0 ? baseUrl + "?" + params.join("&") : baseUrl; } - return { url, queryPath }; + return { assetBaseURL, queryPath }; } private _parseQueryPath(string): string[] { diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index d0c0933e38..a999e09909 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -40,24 +40,24 @@ describe("ResourceManager", () => { describe("queryPath", () => { it("no encode", () => { // @ts-ignore - const { url } = engine.resourceManager._parseURL( + const { assetBaseURL } = engine.resourceManager._parseURL( "https://cdn.ali.com/inner.jpg?x-oss-process=image/resize,l_1024" ); - expect(url).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image/resize,l_1024"); + expect(assetBaseURL).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image/resize,l_1024"); }); it("encode", () => { // @ts-ignore - const { url } = engine.resourceManager._parseURL( + const { assetBaseURL } = engine.resourceManager._parseURL( "https://cdn.ali.com/inner.jpg?x-oss-process=image%25resize,l_1024" ); - expect(url).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image%25resize,l_1024"); + expect(assetBaseURL).equal("https://cdn.ali.com/inner.jpg?x-oss-process=image%25resize,l_1024"); }); it("query path", () => { // @ts-ignore - const { url, queryPath } = engine.resourceManager._parseURL("https://cdn.ali.com/inner.jpg?q=abc"); - expect(url).equal("https://cdn.ali.com/inner.jpg"); + const { assetBaseURL, queryPath } = engine.resourceManager._parseURL("https://cdn.ali.com/inner.jpg?q=abc"); + expect(assetBaseURL).equal("https://cdn.ali.com/inner.jpg"); expect(queryPath).equal("abc"); }); }); From ad21b83432ff7bb4f9ac033fc46f03e27922ba91 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 16 Jun 2026 16:09:18 +0800 Subject: [PATCH 5/6] =?UTF-8?q?test(core):=20fix=20loader=20key=20in=20vir?= =?UTF-8?q?tualPath=20test=20=E2=80=94=20use=20AssetType=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The added test referenced a non-existent "Texture2D" loader key; the texture type is AssetType.Texture = "Texture", so vi.spyOn got undefined and threw "Cannot convert undefined or null to object". Use the AssetType enum for both loader keys to avoid the typo class. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/src/core/resource/ResourceManager.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index a999e09909..2cfe5c4e56 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -92,24 +92,26 @@ describe("ResourceManager", () => { it("infers loader type from virtualPathResourceMap when type is omitted", () => { const resourceManager = engine.resourceManager; resourceManager.initVirtualResources([ - { virtualPath: "Assets/extensionless", path: "https://cdn.ali.com/a.json", type: "Texture2D" } + { virtualPath: "Assets/extensionless", path: "https://cdn.ali.com/a.json", type: AssetType.Texture } ]); // @ts-ignore const loaderSpy = vi - .spyOn(ResourceManager._loaders["Texture2D"], "load") + .spyOn(ResourceManager._loaders[AssetType.Texture], "load") .mockReturnValue(new AssetPromise(() => {})); resourceManager.load({ url: "Assets/extensionless" }); expect(loaderSpy).toHaveBeenCalled(); - expect(loaderSpy.mock.calls[0][0].type).equal("Texture2D"); + expect(loaderSpy.mock.calls[0][0].type).equal(AssetType.Texture); loaderSpy.mockRestore(); }); it("shares the main asset across sub-asset queries", () => { const resourceManager = engine.resourceManager; // @ts-ignore - const loaderSpy = vi.spyOn(ResourceManager._loaders["GLTF"], "load").mockReturnValue(new AssetPromise(() => {})); + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.GLTF], "load") + .mockReturnValue(new AssetPromise(() => {})); resourceManager.load("https://cdn.ali.com/shared.glb?q=materials[0]"); resourceManager.load("https://cdn.ali.com/shared.glb?q=materials[1]"); From 89e5b2868bfae47eefd0e2de6934980456ae8438 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 16 Jun 2026 16:25:36 +0800 Subject: [PATCH 6/6] test(core): cover urls-only / explicit-type / baseUrl cases; use ??= for type - _assignDefaultOptions: type ??= ... (was ||=) so an explicit type is only defaulted when actually unset, not when it is a falsy string - add load()-layer tests: urls merged before parse (KTXCube urls-only), explicit type wins over the virtualPath map type, and a virtualPath still resolves via the map when baseUrl is set Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/asset/ResourceManager.ts | 2 +- .../src/core/resource/ResourceManager.test.ts | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 5e5dd61c45..813775ef07 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -340,7 +340,7 @@ export class ResourceManager { } private _assignDefaultOptions(assetInfo: LoadItem, remoteConfig?: EditorResourceItem): void { - assetInfo.type ||= remoteConfig?.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + assetInfo.type ??= remoteConfig?.type ?? ResourceManager._getTypeByUrl(assetInfo.url); if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index 2cfe5c4e56..8cf6f1569e 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -119,5 +119,59 @@ describe("ResourceManager", () => { expect(loaderSpy).toHaveBeenCalledTimes(1); loaderSpy.mockRestore(); }); + + it("keeps the explicit type over the virtualPath map type", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/explicit", path: "https://cdn.ali.com/x.json", type: AssetType.Texture } + ]); + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.GLTF], "load") + .mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load({ url: "Assets/explicit", type: AssetType.GLTF }); + + expect(loaderSpy).toHaveBeenCalled(); + expect(loaderSpy.mock.calls[0][0].type).equal(AssetType.GLTF); + loaderSpy.mockRestore(); + }); + + it("resolves virtualPath via map even when baseUrl is set", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/withBaseUrl", path: "https://cdn.ali.com/real.json", type: AssetType.Texture } + ]); + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.Texture], "load") + .mockReturnValue(new AssetPromise(() => {})); + resourceManager.baseUrl = "https://base.com/app/"; + + try { + resourceManager.load({ url: "Assets/withBaseUrl" }); + expect(loaderSpy).toHaveBeenCalled(); + expect(loaderSpy.mock.calls[0][0].type).equal(AssetType.Texture); + } finally { + resourceManager.baseUrl = null; + loaderSpy.mockRestore(); + } + }); + + it("merges urls into a single url before parsing", () => { + const resourceManager = engine.resourceManager; + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.KTXCube], "load") + .mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load({ + type: AssetType.KTXCube, + urls: ["px.ktx", "nx.ktx", "py.ktx", "ny.ktx", "pz.ktx", "nz.ktx"] + }); + + expect(loaderSpy).toHaveBeenCalled(); + loaderSpy.mockRestore(); + }); }); });