Skip to content

Commit 8bc0e48

Browse files
author
shuai.jiang
committed
Update configuration for HarmonyOS support by changing Jest preset and enhancing Metro configuration. Add new scripts for Harmony build processes and update package dependencies to align with Harmony template packages. Improve test coverage for document handling and refine portal document download logic for better progress tracking.
1 parent 7a3fb01 commit 8bc0e48

68 files changed

Lines changed: 194704 additions & 1144 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

__tests__/deviceState.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
describe('deviceState platform adapter', () => {
2+
afterEach(() => {
3+
jest.clearAllMocks();
4+
jest.resetModules();
5+
});
6+
7+
test('disables the idle timer on iOS when requested', () => {
8+
const setIdleTimerDisabled = jest.fn();
9+
10+
jest.doMock('react-native', () => ({
11+
NativeModules: {
12+
FPDeviceState: {
13+
setIdleTimerDisabled,
14+
},
15+
},
16+
Platform: {
17+
OS: 'ios',
18+
},
19+
}));
20+
21+
// eslint-disable-next-line @typescript-eslint/no-var-requires
22+
const {setPlatformIdleTimerDisabled} = require('../src/platform/deviceState');
23+
24+
setPlatformIdleTimerDisabled(true);
25+
expect(setIdleTimerDisabled).toHaveBeenCalledWith(true);
26+
});
27+
28+
test('does nothing outside iOS', () => {
29+
const setIdleTimerDisabled = jest.fn();
30+
31+
jest.doMock('react-native', () => ({
32+
NativeModules: {
33+
FPDeviceState: {
34+
setIdleTimerDisabled,
35+
},
36+
},
37+
Platform: {
38+
OS: 'android',
39+
},
40+
}));
41+
42+
// eslint-disable-next-line @typescript-eslint/no-var-requires
43+
const {setPlatformIdleTimerDisabled} = require('../src/platform/deviceState');
44+
45+
setPlatformIdleTimerDisabled(true);
46+
expect(setIdleTimerDisabled).not.toHaveBeenCalled();
47+
});
48+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
describe('Harmony platform adapters', () => {
2+
afterEach(() => {
3+
jest.clearAllMocks();
4+
jest.resetModules();
5+
});
6+
7+
test('uses the file picker as a media-import fallback on Harmony', async () => {
8+
const pickDeviceFilesForShare = jest.fn().mockResolvedValue([{id: 'file'}]);
9+
const pickDeviceMediaForShare = jest.fn();
10+
11+
jest.doMock('react-native', () => ({
12+
Platform: {
13+
OS: 'harmony',
14+
},
15+
}));
16+
jest.doMock('../src/modules/file-access/reactNativeAdapters', () => ({
17+
cleanupImportedDeviceFiles: jest.fn(),
18+
consumePendingSharedItems: jest.fn(),
19+
createReactNativeInboundStorageGateway: jest.fn(),
20+
exportPreparedFile: jest.fn(),
21+
exportStoredFile: jest.fn(),
22+
pickDeviceFilesForShare,
23+
pickDeviceMediaForShare,
24+
}));
25+
26+
// eslint-disable-next-line @typescript-eslint/no-var-requires
27+
const {pickPlatformMediaForShare} = require('../src/platform/fileAccess');
28+
29+
await expect(pickPlatformMediaForShare()).resolves.toEqual([{id: 'file'}]);
30+
expect(pickDeviceFilesForShare).toHaveBeenCalledTimes(1);
31+
expect(pickDeviceMediaForShare).not.toHaveBeenCalled();
32+
});
33+
34+
test('selects the TCP socket runtime on Harmony', () => {
35+
const harmonyRuntime = {
36+
isRunning: jest.fn(),
37+
start: jest.fn(),
38+
};
39+
const nativeRuntime = {
40+
isRunning: jest.fn(),
41+
start: jest.fn(),
42+
};
43+
const createReactNativeTcpHttpRuntime = jest.fn(() => harmonyRuntime);
44+
const createReactNativeHttpRuntime = jest.fn(() => nativeRuntime);
45+
46+
jest.doMock('react-native', () => ({
47+
Platform: {
48+
OS: 'harmony',
49+
},
50+
}));
51+
jest.doMock('../src/modules/service/reactNativeTcpHttpRuntime', () => ({
52+
createReactNativeTcpHttpRuntime,
53+
}));
54+
jest.doMock('../src/modules/service/reactNativeHttpRuntime', () => ({
55+
createReactNativeHttpRuntime,
56+
}));
57+
58+
// eslint-disable-next-line @typescript-eslint/no-var-requires
59+
const {createPlatformServiceRuntime} = require('../src/platform/serviceRuntime');
60+
61+
expect(createPlatformServiceRuntime()).toBe(harmonyRuntime);
62+
expect(createReactNativeTcpHttpRuntime).toHaveBeenCalledTimes(1);
63+
expect(createReactNativeHttpRuntime).not.toHaveBeenCalled();
64+
});
65+
66+
test('writes to the clipboard on Harmony without throwing', () => {
67+
const setString = jest.fn();
68+
69+
jest.doMock('react-native', () => ({
70+
Platform: {
71+
OS: 'harmony',
72+
},
73+
}));
74+
jest.doMock('@react-native-clipboard/clipboard', () => ({
75+
setString,
76+
}));
77+
78+
// eslint-disable-next-line @typescript-eslint/no-var-requires
79+
const {setClipboardString} = require('../src/platform/clipboard');
80+
81+
expect(() => setClipboardString('hello harmony')).not.toThrow();
82+
expect(setString).toHaveBeenCalledWith('hello harmony');
83+
});
84+
});

__tests__/portalDocument.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ describe('buildPortalDocument', () => {
2323
expect(html).toContain("state.phase === 'downloading'");
2424
expect(html).toContain('await Promise.all(');
2525
expect(html).toContain('downloadedChunks[descriptor.index] = chunk;');
26+
expect(html).toContain('response.body?.getReader?.()');
27+
expect(html).toContain('const chunkProgressByIndex = new Array(chunkPlan.length).fill(0);');
2628
expect(html).toContain('setTimeout(() => URL.revokeObjectURL(objectUrl), 60000);');
2729
expect(html).not.toContain('浏览器投递');
2830
expect(html).not.toContain('hero-copy');

__tests__/reactNativeAdapters.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,97 @@ describe('ReactNativeFileSystemAdapter', () => {
189189
'base64',
190190
);
191191
});
192+
193+
test('reads pending shared items from the Harmony inbox manifest when the native bridge is unavailable', async () => {
194+
jest.resetModules();
195+
196+
const exists = jest.fn().mockResolvedValue(true);
197+
const readFile = jest.fn().mockResolvedValue(
198+
JSON.stringify({
199+
files: [
200+
{
201+
byteLength: 12,
202+
createdAt: '2026-04-29T12:00:00.000Z',
203+
mimeType: 'text/plain',
204+
name: 'notes.txt',
205+
relativePath: 'notes.txt',
206+
sourcePath: '/sandbox/cache/ffb-inbound/notes.txt',
207+
},
208+
],
209+
texts: [
210+
{
211+
content: 'Shared note',
212+
createdAt: '2026-04-29T12:00:00.000Z',
213+
},
214+
],
215+
}),
216+
);
217+
const unlink = jest.fn().mockResolvedValue(undefined);
218+
219+
jest.doMock('react-native', () => ({
220+
NativeModules: {},
221+
Platform: {
222+
OS: 'harmony',
223+
},
224+
}));
225+
jest.doMock('../src/platform/documentPicker', () => ({
226+
documentPickerErrorCodes: {
227+
OPERATION_CANCELED: 'DOCUMENT_PICKER_CANCELED',
228+
},
229+
documentPickerTypes: {
230+
allFiles: '*/*',
231+
},
232+
isDocumentPickerErrorWithCode: jest.fn(),
233+
pickDocuments: jest.fn(),
234+
savePickedDocuments: jest.fn(),
235+
}));
236+
jest.doMock('react-native-share', () => ({
237+
__esModule: true,
238+
default: {},
239+
}));
240+
jest.doMock('react-native-fs', () => ({
241+
__esModule: true,
242+
default: {
243+
DocumentDirectoryPath: '/sandbox/files',
244+
exists,
245+
mkdir: jest.fn(),
246+
readFile,
247+
unlink,
248+
},
249+
}));
250+
jest.doMock('pako', () => ({
251+
gzip: jest.fn(),
252+
ungzip: jest.fn(),
253+
}));
254+
255+
// eslint-disable-next-line @typescript-eslint/no-var-requires
256+
const {consumePendingSharedItems} = require('../src/modules/file-access/reactNativeAdapters');
257+
258+
await expect(consumePendingSharedItems()).resolves.toEqual({
259+
files: [
260+
{
261+
byteLength: 12,
262+
cleanupPath: '/sandbox/cache/ffb-inbound/notes.txt',
263+
createdAt: '2026-04-29T12:00:00.000Z',
264+
mimeType: 'text/plain',
265+
name: 'notes.txt',
266+
relativePath: 'notes.txt',
267+
sourcePath: '/sandbox/cache/ffb-inbound/notes.txt',
268+
},
269+
],
270+
texts: [
271+
{
272+
content: 'Shared note',
273+
createdAt: '2026-04-29T12:00:00.000Z',
274+
},
275+
],
276+
});
277+
278+
expect(exists).toHaveBeenCalledWith('/sandbox/files/ffb-inbound/pending.json');
279+
expect(readFile).toHaveBeenCalledWith(
280+
'/sandbox/files/ffb-inbound/pending.json',
281+
'utf8',
282+
);
283+
expect(unlink).toHaveBeenCalledWith('/sandbox/files/ffb-inbound/pending.json');
284+
});
192285
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
encodeHttpResponse,
3+
parseHttpRequestFrame,
4+
} from '../src/modules/service/reactNativeTcpHttpRuntime';
5+
6+
function decodeAscii(bytes: Uint8Array) {
7+
return Array.from(bytes)
8+
.map(value => String.fromCharCode(value))
9+
.join('');
10+
}
11+
12+
describe('parseHttpRequestFrame', () => {
13+
test('returns null until the full request body arrives', () => {
14+
const partial = new TextEncoder().encode(
15+
'POST /api/text HTTP/1.1\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhe',
16+
);
17+
18+
expect(parseHttpRequestFrame(partial)).toBeNull();
19+
});
20+
21+
test('parses method, query, headers, and text body', () => {
22+
const requestBytes = new TextEncoder().encode(
23+
'POST /api/text?key=abc HTTP/1.1\r\nContent-Type: text/plain; charset=utf-8\r\nX-Client-Id: browser-a\r\nContent-Length: 5\r\n\r\nhello',
24+
);
25+
26+
const parsed = parseHttpRequestFrame(requestBytes);
27+
28+
expect(parsed).not.toBeNull();
29+
expect(parsed?.consumedBytes).toBe(requestBytes.byteLength);
30+
expect(parsed?.request.method).toBe('POST');
31+
expect(parsed?.request.path).toBe('/api/text');
32+
expect(parsed?.request.query.get('key')).toBe('abc');
33+
expect(parsed?.request.headers['x-client-id']).toBe('browser-a');
34+
expect(parsed?.request.body).toBe('hello');
35+
});
36+
});
37+
38+
describe('encodeHttpResponse', () => {
39+
test('adds content length and connection headers for json responses', () => {
40+
const responseBytes = encodeHttpResponse({
41+
body: {
42+
ok: true,
43+
},
44+
headers: {
45+
'content-type': 'application/json; charset=utf-8',
46+
},
47+
status: 200,
48+
});
49+
50+
const headerEnd = decodeAscii(responseBytes).indexOf('\r\n\r\n');
51+
expect(headerEnd).toBeGreaterThan(0);
52+
53+
const headerText = decodeAscii(responseBytes.slice(0, headerEnd));
54+
const bodyText = new TextDecoder('utf-8').decode(
55+
responseBytes.slice(headerEnd + 4),
56+
);
57+
58+
expect(headerText).toContain('HTTP/1.1 200 OK');
59+
expect(headerText).toContain('content-type: application/json; charset=utf-8');
60+
expect(headerText).toContain('connection: close');
61+
expect(headerText).toContain(`content-length: ${bodyText.length}`);
62+
expect(bodyText).toBe(JSON.stringify({ok: true}));
63+
});
64+
65+
test('encodes base64-backed binary payloads', () => {
66+
const responseBytes = encodeHttpResponse({
67+
body: {
68+
base64: Buffer.from('chunk-data', 'utf8').toString('base64'),
69+
byteLength: 10,
70+
kind: 'base64',
71+
},
72+
headers: {
73+
'content-type': 'application/octet-stream',
74+
},
75+
status: 206,
76+
});
77+
78+
const headerEnd = decodeAscii(responseBytes).indexOf('\r\n\r\n');
79+
const bodyBytes = responseBytes.slice(headerEnd + 4);
80+
81+
expect(decodeAscii(responseBytes.slice(0, headerEnd))).toContain(
82+
'HTTP/1.1 206 Partial Content',
83+
);
84+
expect(Buffer.from(bodyBytes).toString('utf8')).toBe('chunk-data');
85+
});
86+
});

0 commit comments

Comments
 (0)