diff --git a/src/adapters/cmcc/adapter.ts b/src/adapters/cmcc/adapter.ts index 30d6403..2dc1951 100644 --- a/src/adapters/cmcc/adapter.ts +++ b/src/adapters/cmcc/adapter.ts @@ -68,16 +68,18 @@ export class CMCCAdapter extends SharedCloudDriveAdapter { startNumber: cursor.type === 'initial' ? 1 : cursor.value, }; - const response = await this.callAPI( + const response = this.normalizeResponse(await this.callAPI( 'POST', 'https://personal-kd-njs.yun.139.com/hcy/file/list', this.createSignedJsonBody(payload, signPayload) - ) as CMCCListResponse; - this.assertSuccessful(String(response.code ?? '0') === '0' || String(response.code ?? '0') === '200', response.code, response.message || '移动云盘列表获取失败', response); + )); + this.assertSuccessful(this.isSuccessfulResponseCode(response.code), response.code, response.message || '移动云盘列表获取失败', response); const list = response.data?.items; if (!Array.isArray(list)) { - throw new Error('移动云盘列表获取失败: missing file list'); + const responseKeys = Object.keys(response || {}).join(',') || 'none'; + const dataKeys = response.data ? Object.keys(response.data).join(',') || 'none' : 'none'; + throw new Error(`移动云盘列表获取失败: missing file list (response keys: ${responseKeys}; data keys: ${dataKeys})`); } files.push(...list.filter((item) => item.type !== 'folder').map((item) => this.toFileItem(item, targetParentId))); @@ -98,13 +100,12 @@ export class CMCCAdapter extends SharedCloudDriveAdapter { contentID: fileId, contentName: newName, }; - const response = await this.callAPI( + const response = this.normalizeResponse(await this.callAPI( 'POST', 'https://personal-kd-njs.yun.139.com/hcy/file/update', this.createSignedJsonBody(payload, signPayload) - ) as CMCCUpdateResponse; - const code = String(response.code ?? '0'); - this.assertSuccessful(code === '0' || code === '200' || code.toLowerCase() === 'success', response.code, response.message || '移动云盘重命名失败', response); + )); + this.assertSuccessful(this.isSuccessfulResponseCode(response.code), response.code, response.message || '移动云盘重命名失败', response); }, fileId, newName); } @@ -116,6 +117,40 @@ export class CMCCAdapter extends SharedCloudDriveAdapter { return getCMCCPageScriptInjector(); } + private isSuccessfulResponseCode(code: string | number | undefined): boolean { + const normalized = String(code ?? '0').toLowerCase(); + return normalized === '0' || normalized === '0000' || normalized === '200' || normalized === 'success'; + } + + private normalizeResponse(response: unknown): T { + let current = response; + + for (let attempt = 0; attempt < 3; attempt++) { + if (typeof current === 'string') { + const text = current.trim(); + if (!text) return {} as T; + try { + current = JSON.parse(text) as unknown; + continue; + } catch { + break; + } + } + + if (current && typeof current === 'object' && !Array.isArray(current)) { + const text = (current as { text?: unknown }).text; + if (typeof text === 'string') { + current = text; + continue; + } + } + + break; + } + + return current as T; + } + private toFileItem(item: CMCCFileItem, parentId: string): FileItem { const name = item.name || ''; const parsed = parseFileName(name); diff --git a/src/adapters/cmcc/page-script.ts b/src/adapters/cmcc/page-script.ts index b5c9373..51cfcdb 100644 --- a/src/adapters/cmcc/page-script.ts +++ b/src/adapters/cmcc/page-script.ts @@ -1,12 +1,13 @@ -import { installCloudDrivePageScript } from '../shared/page-script'; +import { installCloudDrivePageScript, type PageScriptInstallOptions } from '../shared/page-script'; -export const CMCC_PAGE_SCRIPT_OPTIONS = { +export const CMCC_PAGE_SCRIPT_OPTIONS: PageScriptInstallOptions = { requestType: 'CMCC_API_REQUEST', responseType: 'CMCC_API_RESPONSE', readyFlagName: '__CMCC_PAGE_SCRIPT_READY__', datasetReadyKey: 'cmccPageScriptReady', datasetTimestampKey: 'cmccPageScriptTimestamp', logPrefix: 'CMCCPageScript', + transport: 'xhr', captureHeaders: [ 'caller', 'Cms-Device', @@ -20,7 +21,6 @@ export const CMCC_PAGE_SCRIPT_OPTIONS = { 'x-inner-ntwk', 'x-m4c-caller', 'INNER-HCY-ROUTER-HTTPS', - 'hcy-cool-flag', 'X-Svctype', 'x-yun-Api-Version', 'x-yun-channel-source', diff --git a/src/adapters/shared/page-script.ts b/src/adapters/shared/page-script.ts index 0c87a03..aaf6952 100644 --- a/src/adapters/shared/page-script.ts +++ b/src/adapters/shared/page-script.ts @@ -6,6 +6,7 @@ export interface PageScriptInstallOptions { datasetTimestampKey: string; logPrefix: string; captureHeaders?: string[]; + transport?: 'fetch' | 'xhr'; } interface APIRequestMessage { @@ -98,6 +99,65 @@ export function markPageScriptReady(options: PageScriptInstallOptions, timestamp return true; } +function parseResponseText(text: string): unknown { + if (!text) return {}; + try { + return JSON.parse(text); + } catch { + return { text }; + } +} + +async function requestWithFetch( + message: Partial, + encoded: { body?: BodyInit; headers?: Record }, + capturedHeaders: Map, + signal: AbortSignal +): Promise<{ success: boolean; status: number; body: unknown }> { + const headers = new Headers(); + for (const [key, value] of capturedHeaders.entries()) headers.set(key, value); + appendHeaders(headers, encoded.headers); + + const response = await fetch(message.url || '', { + method: message.method, + credentials: 'include', + headers, + body: message.method?.toUpperCase() === 'GET' ? undefined : encoded.body, + signal, + }); + const text = await response.text(); + return { success: response.ok, status: response.status, body: parseResponseText(text) }; +} + +function requestWithXhr( + message: Partial, + encoded: { body?: BodyInit; headers?: Record }, + capturedHeaders: Map +): Promise<{ success: boolean; status: number; body: unknown }> { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open(message.method || 'GET', message.url || '', true); + xhr.withCredentials = true; + if (message.timeout) xhr.timeout = message.timeout; + + for (const [key, value] of capturedHeaders.entries()) xhr.setRequestHeader(key, value); + for (const [key, value] of Object.entries(encoded.headers || {})) xhr.setRequestHeader(key, value); + + xhr.onreadystatechange = () => { + if (xhr.readyState !== XMLHttpRequest.DONE) return; + resolve({ + success: xhr.status >= 200 && xhr.status < 300, + status: xhr.status, + body: parseResponseText(xhr.responseText || ''), + }); + }; + xhr.onerror = () => reject(new Error(`XHR request failed: ${message.url}`)); + xhr.ontimeout = () => reject(new Error(`XHR request timeout: ${message.url}`)); + + xhr.send(message.method?.toUpperCase() === 'GET' ? undefined : (encoded.body as XMLHttpRequestBodyInit | undefined)); + }); +} + export function installCloudDrivePageScript(options: PageScriptInstallOptions): void { const capturedHeaders = new Map(); const allowHeader = new Set((options.captureHeaders || []).map(normalizeHeaderName)); @@ -133,35 +193,17 @@ export function installCloudDrivePageScript(options: PageScriptInstallOptions): try { const encoded = createBodyAndHeaders(message.body); - const headers = new Headers(); - for (const [key, value] of capturedHeaders.entries()) headers.set(key, value); - appendHeaders(headers, encoded.headers); - - const response = await fetch(message.url, { - method: message.method, - credentials: 'include', - headers, - body: message.method.toUpperCase() === 'GET' ? undefined : encoded.body, - signal: controller.signal, - }); + const data = options.transport === 'xhr' + ? await requestWithXhr(message, encoded, capturedHeaders) + : await requestWithFetch(message, encoded, capturedHeaders, controller.signal); window.clearTimeout(timeoutId); - const text = await response.text(); - let data: unknown = {}; - if (text) { - try { - data = JSON.parse(text); - } catch { - data = { text }; - } - } - window.postMessage({ type: options.responseType, requestId: message.requestId, - success: response.ok, - data, - status: response.status, + success: data.success, + data: data.body, + status: data.status, }, '*'); } catch (error) { window.clearTimeout(timeoutId); diff --git a/src/adapters/shared/shared-cloud-drive-adapter.ts b/src/adapters/shared/shared-cloud-drive-adapter.ts index d1e1ffd..047a85b 100644 --- a/src/adapters/shared/shared-cloud-drive-adapter.ts +++ b/src/adapters/shared/shared-cloud-drive-adapter.ts @@ -21,6 +21,7 @@ export class CloudDriveAPIError extends Error { export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { private lastRequestTime = 0; private pendingConflictListRequests = new Map>; + private static activeDomSyncCleanup: (() => void) | null = null; protected constructor(config: Partial) { super({ @@ -156,6 +157,8 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { return { success: true, method: 'none' }; } + this.disconnectActiveDomSyncObserver(); + const documents = this.getDomPatchDocuments(); let patchedById = 0; let patchedByOldName = 0; @@ -251,7 +254,7 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { const info = renameInfoById.get(fileId); if (!info) continue; - const row = node.closest('tr,[role="row"],.ant-table-row,.file-item,.list-item,li[rel="item"],li') || node; + const row = node.closest('tr,[role="row"],.ant-table-row,.document_table_list,.file-item,.list-item,li[rel="item"],li') || node; patched += this.patchRowElement(row, info.oldName, info.newName); } @@ -262,7 +265,7 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { if (renameByOldName.size === 0) return 0; let patched = 0; - const nodes = Array.from(root.querySelectorAll('.file-name,.filename,.name,.table-file-name,.table-file-name-tooltip-host,.table-list-file-name,[title],[aria-label]')); + const nodes = Array.from(root.querySelectorAll('.touch-div,.document_table_list_name_text,.file-name,.filename,.name,.table-file-name,.table-file-name-tooltip-host,.table-list-file-name,[title],[aria-label]')); for (const node of nodes) { for (const [oldName, newName] of renameByOldName.entries()) { patched += this.patchNameElement(node, oldName, newName); @@ -272,7 +275,7 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { } private patchRowElement(row: Element, oldName: string | undefined, newName: string): number { - const preferredNameNode = row.querySelector('.table-file-name,.table-file-name-tooltip-host,.table-list-file-name,.file-name,.filename,.name'); + const preferredNameNode = row.querySelector('.touch-div,.document_table_list_name_text,.table-file-name,.table-file-name-tooltip-host,.table-list-file-name,.file-name,.filename,.name'); if (preferredNameNode && oldName) { return this.patchStructuredNameWithinElement(preferredNameNode, oldName, newName); } @@ -393,7 +396,7 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { const roots = new Set(); const ownerWindow = doc.defaultView ?? window; const NodeCtor = ownerWindow.Node; - const rowSelector = 'tr,[role="row"],.ant-table-row,.file-item,.list-item,li[rel="item"],li'; + const rowSelector = 'tr,[role="row"],.ant-table-row,.document_table_list,.file-item,.list-item,li[rel="item"],li'; const addNode = (node: Node | null) => { if (!node) return; @@ -412,6 +415,12 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { return Array.from(roots); } + private disconnectActiveDomSyncObserver(): void { + const cleanup = SharedCloudDriveAdapter.activeDomSyncCleanup; + SharedCloudDriveAdapter.activeDomSyncCleanup = null; + cleanup?.(); + } + private observeAndPatchRenamedRows( renameInfoById: Map, renameByOldName: Map, @@ -458,14 +467,29 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { cleanupCallbacks.push(() => { if (flushTimer !== null) { observerWindow.clearTimeout(flushTimer); + flushTimer = null; } }); } - window.setTimeout(() => { - for (const cleanup of cleanupCallbacks) cleanup(); + let timeoutId: number | null = null; + let cleaned = false; + const cleanup = () => { + if (cleaned) return; + cleaned = true; + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + timeoutId = null; + } + for (const clearPendingFlush of cleanupCallbacks) clearPendingFlush(); for (const observer of observers) observer.disconnect(); - }, durationMs); + if (SharedCloudDriveAdapter.activeDomSyncCleanup === cleanup) { + SharedCloudDriveAdapter.activeDomSyncCleanup = null; + } + }; + + timeoutId = window.setTimeout(cleanup, durationMs); + SharedCloudDriveAdapter.activeDomSyncCleanup = cleanup; } private tryTriggerNativeFileListRerender(documents: Document[] = this.getDomPatchDocuments()): { triggered: boolean; message?: string } { @@ -496,7 +520,7 @@ export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter { private findFilenameInRow(row: Element): string { return ( - row.querySelector('.table-file-name,.table-file-name-tooltip-host,.table-list-file-name,.file-name,.filename,.name,[title]')?.textContent?.trim() || + row.querySelector('.touch-div,.document_table_list_name_text,.table-file-name,.table-file-name-tooltip-host,.table-list-file-name,.file-name,.filename,.name,[title]')?.textContent?.trim() || row.getAttribute('title') || row.getAttribute('aria-label') || '' diff --git a/src/utils/platform-detector.ts b/src/utils/platform-detector.ts index 0cdf2f0..b3dce10 100644 --- a/src/utils/platform-detector.ts +++ b/src/utils/platform-detector.ts @@ -108,7 +108,8 @@ export function detectPlatformFromUrl(url: string, pathname?: string): PlatformN // 移动云盘 if (url.includes('yun.139.com')) { - return url.includes('/w/#/main') || url.includes('/w/#/index') ? 'cmcc' : null; + const pathToCheck = pathname ?? extractPathnameFromUrl(url); + return pathToCheck === '/w/' || url.includes('/w/#/main') || url.includes('/w/#/index') ? 'cmcc' : null; } // 天翼云盘 diff --git a/tests/unit/adapters/missing-platform-adapters.test.ts b/tests/unit/adapters/missing-platform-adapters.test.ts index 951f02c..e79f49d 100644 --- a/tests/unit/adapters/missing-platform-adapters.test.ts +++ b/tests/unit/adapters/missing-platform-adapters.test.ts @@ -6,6 +6,7 @@ import { EsurfingAdapter } from '../../../src/adapters/esurfing/adapter'; import { getDrive115PageScriptInjector } from '../../../src/adapters/115/page-script-injector'; import { getDrive123PageScriptInjector } from '../../../src/adapters/123/page-script-injector'; import { getCMCCPageScriptInjector } from '../../../src/adapters/cmcc/page-script-injector'; +import { CMCC_PAGE_SCRIPT_OPTIONS } from '../../../src/adapters/cmcc/page-script'; import { getEsurfingPageScriptInjector } from '../../../src/adapters/esurfing/page-script-injector'; vi.mock('../../../src/adapters/115/page-script-injector', () => ({ @@ -133,6 +134,38 @@ describe('missing platform adapters', () => { ); }); + it('does not duplicate CMCC explicit auth headers or capture encryption-triggering headers', () => { + const headers = CMCC_PAGE_SCRIPT_OPTIONS.captureHeaders?.map((header) => header.toLowerCase()); + expect(headers).not.toContain('authorization'); + expect(headers).not.toContain('hcy-cool-flag'); + }); + + it('parses CMCC list responses that arrive as JSON strings', async () => { + const adapter = new CMCCAdapter({ requestInterval: 0 }); + localStorage.setItem('currentCatalogID', 'folder139'); + mockCMCCCallAPI.mockResolvedValue(JSON.stringify({ + code: '0000', + message: '请求成功', + data: { + items: [{ fileId: 'file-139', name: 'Episode 01.mkv', parentFileId: 'folder139', fileExtension: 'mkv', size: 56, updatedAt: 789, type: 'file' }], + nextPageCursor: null, + }, + })); + + const files = await adapter.getAllFiles(); + + expect(files[0]).toEqual({ id: 'file-139', name: 'Episode 01.mkv', ext: '.mkv', parentId: 'folder139', size: 56, mtime: 789 }); + }); + + it('accepts CMCC rename responses with the real success code', async () => { + const adapter = new CMCCAdapter({ requestInterval: 0 }); + mockCMCCCallAPI.mockResolvedValue({ code: '0000', message: '请求成功' }); + + const result = await adapter.renameFile('file-139', 'Episode 01-fixed.mkv'); + + expect(result).toEqual({ success: true, newName: 'Episode 01-fixed.mkv' }); + }); + it('uses CMCC APIs and deduplicates concurrent conflict checks', async () => { const adapter = new CMCCAdapter({ requestInterval: 0 }); Object.defineProperty(window, 'location', { @@ -223,6 +256,14 @@ describe('missing platform adapters', () => { await expect(adapter.getAllFiles()).rejects.toThrow('list failed'); }); + it('reports CMCC response shape when the list payload is missing', async () => { + const adapter = new CMCCAdapter({ requestInterval: 0 }); + localStorage.setItem('currentCatalogID', 'folder139'); + mockCMCCCallAPI.mockResolvedValue({ code: 0, data: { catalogList: [] } }); + + await expect(adapter.getAllFiles()).rejects.toThrow('response keys: code,data; data keys: catalogList'); + }); + it('throws instead of returning an empty list when Esurfing list API fails', async () => { const adapter = new EsurfingAdapter({ requestInterval: 0 }); Object.defineProperty(window, 'location', { @@ -342,6 +383,65 @@ describe('missing platform adapters', () => { expect(nameNode?.textContent).toBe('WADBS_1.3.apk'); }); + it('patches CMCC visible rows with document table filename classes after rename', async () => { + document.body.innerHTML = ` +
+
+
+
+
未命名项目-图层 1.png
+ 未命名项目-图层 1.png +
+
+
今天 11:40
+
246.59KB
+
+
+ `; + + const adapter = new CMCCAdapter({ requestInterval: 0 }); + const result = await adapter.syncAfterRename([ + { fileId: 'file-139', oldName: '未命名项目-图层 1.png', newName: 'test-未命名项目-图层 1.png' }, + ]); + + const nameNode = document.querySelector('.touch-div'); + expect(result.success).toBe(true); + expect(result.method).toBe('dom-patch'); + expect(nameNode?.textContent).toBe('test-未命名项目-图层 1.png'); + }); + + it('disconnects older DOM sync observers before undo sync starts', async () => { + document.body.innerHTML = '
'; + + const adapter = new CMCCAdapter({ requestInterval: 0 }); + await adapter.syncAfterRename([ + { fileId: 'file-139', oldName: '未命名项目-图层 1.png', newName: 'test-未命名项目-图层 1.png' }, + ]); + await adapter.syncAfterRename([ + { fileId: 'file-139', oldName: 'test-未命名项目-图层 1.png', newName: '未命名项目-图层 1.png' }, + ]); + + const observedNames: string[] = []; + const observer = new MutationObserver(() => { + const text = document.querySelector('.touch-div')?.textContent?.trim(); + if (text) observedNames.push(text); + }); + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + + document.querySelector('#file-list')!.insertAdjacentHTML('beforeend', ` +
+
+
未命名项目-图层 1.png
+
+
+ `); + await new Promise((resolve) => window.setTimeout(resolve, 20)); + observer.disconnect(); + + expect(observedNames).not.toContain('test-未命名项目-图层 1.png'); + expect(document.querySelector('.touch-div')?.textContent).toBe('未命名项目-图层 1.png'); + }); + it('patches later 123Pan row mutations without rescanning the whole document', async () => { document.body.innerHTML = '
'; diff --git a/tests/unit/adapters/shared-page-script.test.ts b/tests/unit/adapters/shared-page-script.test.ts index 1dc8598..7286f12 100644 --- a/tests/unit/adapters/shared-page-script.test.ts +++ b/tests/unit/adapters/shared-page-script.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; -import { markPageScriptReady } from '../../../src/adapters/shared/page-script'; +import { installCloudDrivePageScript, markPageScriptReady } from '../../../src/adapters/shared/page-script'; +import { CMCC_PAGE_SCRIPT_OPTIONS } from '../../../src/adapters/cmcc/page-script'; describe('shared cloud drive page script', () => { afterEach(() => { @@ -22,4 +23,82 @@ describe('shared cloud drive page script', () => { Object.defineProperty(document, 'body', { value: body, configurable: true }); }); + + it('uses XHR transport for CMCC so request header casing is preserved', () => { + expect(CMCC_PAGE_SCRIPT_OPTIONS.transport).toBe('xhr'); + }); + + it('sends API requests with original header casing in XHR transport', async () => { + const sentHeaders: Array<[string, string]> = []; + const openedRequests: Array<{ method: string; url: string }> = []; + + class FakeXHR { + static DONE = 4; + readyState = 0; + status = 200; + responseText = '{"success":true}'; + withCredentials = false; + onreadystatechange: (() => void) | null = null; + onerror: (() => void) | null = null; + ontimeout: (() => void) | null = null; + timeout = 0; + + open(method: string, url: string): void { + openedRequests.push({ method, url }); + } + + setRequestHeader(name: string, value: string): void { + sentHeaders.push([name, value]); + } + + send(): void { + this.readyState = FakeXHR.DONE; + this.onreadystatechange?.(); + } + } + + const originalXHR = window.XMLHttpRequest; + Object.defineProperty(window, 'XMLHttpRequest', { value: FakeXHR, configurable: true }); + + installCloudDrivePageScript({ + requestType: 'TEST_XHR_REQUEST', + responseType: 'TEST_XHR_RESPONSE', + readyFlagName: '__TEST_XHR_READY__', + datasetReadyKey: 'testXhrReady', + datasetTimestampKey: 'testXhrTimestamp', + logPrefix: 'TestXHR', + transport: 'xhr', + }); + + const requestEvent = new MessageEvent('message', { + data: { + type: 'TEST_XHR_REQUEST', + requestId: 'request-1', + method: 'POST', + url: 'https://example.com/api', + body: { + bodyMode: 'json', + data: { ok: true }, + headers: { + 'CMS-DEVICE': 'default', + 'INNER-HCY-ROUTER-HTTPS': '1', + 'X-SvcType': '1', + }, + }, + }, + }); + Object.defineProperty(requestEvent, 'source', { value: window }); + window.dispatchEvent(requestEvent); + await Promise.resolve(); + + expect(openedRequests).toEqual([{ method: 'POST', url: 'https://example.com/api' }]); + expect(sentHeaders).toEqual(expect.arrayContaining([ + ['Content-Type', 'application/json;charset=UTF-8'], + ['CMS-DEVICE', 'default'], + ['INNER-HCY-ROUTER-HTTPS', '1'], + ['X-SvcType', '1'], + ])); + + Object.defineProperty(window, 'XMLHttpRequest', { value: originalXHR, configurable: true }); + }); }); diff --git a/tests/unit/manifest-platform-matches.test.ts b/tests/unit/manifest-platform-matches.test.ts index c2d0672..40ba9f8 100644 --- a/tests/unit/manifest-platform-matches.test.ts +++ b/tests/unit/manifest-platform-matches.test.ts @@ -33,4 +33,15 @@ describe('manifest platform matches', () => { expect(webAccessibleMatches).toContain('https://yun.123pan.cn/*'); expect(webAccessibleMatches).not.toContain('https://www.123pan.com/*'); }); + + it('injects CMCC UI on yun.139.com and allows its API host', () => { + const manifest = loadManifest(); + const contentScriptMatches = manifest.content_scripts?.flatMap((script) => script.matches ?? []) ?? []; + const webAccessibleMatches = manifest.web_accessible_resources?.flatMap((resource) => resource.matches ?? []) ?? []; + + expect(manifest.host_permissions).toContain('https://yun.139.com/*'); + expect(manifest.host_permissions).toContain('https://personal-kd-njs.yun.139.com/*'); + expect(contentScriptMatches).toContain('https://yun.139.com/*'); + expect(webAccessibleMatches).toContain('https://yun.139.com/*'); + }); }); diff --git a/tests/unit/platform-detection.test.ts b/tests/unit/platform-detection.test.ts index e5b621f..598eeca 100644 --- a/tests/unit/platform-detection.test.ts +++ b/tests/unit/platform-detection.test.ts @@ -226,6 +226,22 @@ describe('Platform Detection - Unified Utils', () => { expect(result).toBe('cmcc'); }); + it('should detect CMCC Drive on the SPA index route', () => { + const result = detectPlatformFromUrl( + 'https://yun.139.com/w/#/index', + '/w/' + ); + expect(result).toBe('cmcc'); + }); + + it('should detect CMCC Drive on the app entry before hash routing is ready', () => { + const result = detectPlatformFromUrl( + 'https://yun.139.com/w/', + '/w/' + ); + expect(result).toBe('cmcc'); + }); + it('should detect Esurfing Drive', () => { const result = detectPlatformFromUrl( 'https://cloud.189.cn/web/main/file/folder/folder189',