镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
refactor(cmcc): 改进API调用响应解析与平台检测
- 添加 `normalizeResponse` 方法处理嵌套 JSON 字符串响应 - 统一成功状态码验证逻辑 - 使用 XHR 传输保留请求头大小写 - 修复 SPA 根路径 `/w/` 的平台检测 - 添加 DOM 同步观察器断开逻辑
这个提交包含在:
父节点
cb32e8aa55
当前提交
cefc989c20
@ -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<CMCCListResponse>(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<CMCCUpdateResponse>(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<T>(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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<APIRequestMessage>,
|
||||
encoded: { body?: BodyInit; headers?: Record<string, string> },
|
||||
capturedHeaders: Map<string, string>,
|
||||
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<APIRequestMessage>,
|
||||
encoded: { body?: BodyInit; headers?: Record<string, string> },
|
||||
capturedHeaders: Map<string, string>
|
||||
): 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<string, string>();
|
||||
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);
|
||||
|
||||
@ -21,6 +21,7 @@ export class CloudDriveAPIError extends Error {
|
||||
export abstract class SharedCloudDriveAdapter extends BasePlatformAdapter {
|
||||
private lastRequestTime = 0;
|
||||
private pendingConflictListRequests = new Map<string, Promise<FileItem[]>>;
|
||||
private static activeDomSyncCleanup: (() => void) | null = null;
|
||||
|
||||
protected constructor(config: Partial<PlatformConfig>) {
|
||||
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<ParentNode>();
|
||||
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<string, { oldName?: string; newName: string }>,
|
||||
renameByOldName: Map<string, string>,
|
||||
@ -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') ||
|
||||
''
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
// 天翼云盘
|
||||
|
||||
@ -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 = `
|
||||
<div class="main_file_list">
|
||||
<div class="document_table_list">
|
||||
<div class="document_table_list_name name-col-3-item">
|
||||
<div class="document_table_list_name_text">
|
||||
<div class="touch-div"><span>未命名项目-图层 1.png</span></div>
|
||||
<span class="mirror-name">未命名项目-图层 1.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="document_table_list_time">今天 11:40</div>
|
||||
<div class="document_table_list_size">246.59KB</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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<HTMLElement>('.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 = '<main id="file-list"></main>';
|
||||
|
||||
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<HTMLElement>('.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', `
|
||||
<div class="document_table_list">
|
||||
<div class="document_table_list_name_text">
|
||||
<div class="touch-div"><span>未命名项目-图层 1.png</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 20));
|
||||
observer.disconnect();
|
||||
|
||||
expect(observedNames).not.toContain('test-未命名项目-图层 1.png');
|
||||
expect(document.querySelector<HTMLElement>('.touch-div')?.textContent).toBe('未命名项目-图层 1.png');
|
||||
});
|
||||
|
||||
it('patches later 123Pan row mutations without rescanning the whole document', async () => {
|
||||
document.body.innerHTML = '<main id="file-list"></main>';
|
||||
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -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/*');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户