@flighthq/share 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export * from './share';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './share';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { ShareBackend, ShareContent, ShareOptions, ShareResult, ShareSignals } from '@flighthq/types';
2
+ export declare function attachShareSignals(signals: ShareSignals): void;
3
+ export declare function canShareContent(content: Readonly<ShareContent>): boolean;
4
+ export declare function createWebShareBackend(): ShareBackend;
5
+ export declare function detachShareSignals(signals: ShareSignals): void;
6
+ export declare function disposeShareSignals(signals: ShareSignals): void;
7
+ export declare function enableShareSignals(): ShareSignals;
8
+ export declare function getShareBackend(): ShareBackend;
9
+ export declare function hasShareContentFields(content: Readonly<ShareContent>): boolean;
10
+ export declare function isShareAvailable(): boolean;
11
+ export declare function setShareBackend(backend: ShareBackend | null): void;
12
+ export declare function shareContent(content: Readonly<ShareContent>, options?: Readonly<ShareOptions>): Promise<boolean>;
13
+ export declare function shareContentWithResult(content: Readonly<ShareContent>, options?: Readonly<ShareOptions>): Promise<ShareResult>;
14
+ export declare function shareText(text: string, options?: Readonly<ShareOptions>): Promise<boolean>;
15
+ export declare function shareUrl(url: string, options?: Readonly<ShareOptions>): Promise<boolean>;
16
+ //# sourceMappingURL=share.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAS3G,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAG9D;AAMD,wBAAgB,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAExE;AAQD,wBAAgB,qBAAqB,IAAI,YAAY,CA4CpD;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE9D;AAID,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE/D;AAID,wBAAgB,kBAAkB,IAAI,YAAY,CAIjD;AAGD,wBAAgB,eAAe,IAAI,YAAY,CAG9C;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAM9E;AAKD,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAGD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAElE;AAOD,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGhH;AAMD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC/B,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAC/B,OAAO,CAAC,WAAW,CAAC,CAWtB;AAGD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAE1F;AAGD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAExF"}
package/dist/share.js ADDED
@@ -0,0 +1,194 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ // Attaches `signals` to receive share result events emitted by shareContentWithResult calls. A prior
3
+ // subscription on this signals group is torn down first. Pair with detachShareSignals /
4
+ // disposeShareSignals.
5
+ //
6
+ // Note: shareContent (the boolean convenience wrapper) does NOT emit signals — only
7
+ // shareContentWithResult emits onShareResult. If you need signals on every share, use
8
+ // shareContentWithResult directly.
9
+ export function attachShareSignals(signals) {
10
+ detachShareSignals(signals);
11
+ _signalListeners.set(signals, true);
12
+ }
13
+ // True when the active backend can share the given content. Returns false when sharing is
14
+ // unavailable or the content is not shareable by this platform (e.g. a MIME type not accepted by
15
+ // the share sheet). Distinct from isShareAvailable: that probe asks whether sharing is possible at
16
+ // all; this asks whether this specific content is shareable.
17
+ export function canShareContent(content) {
18
+ return getShareBackend().canShare(content);
19
+ }
20
+ // Builds the default web backend over navigator.share / navigator.canShare.
21
+ // share and shareWithResult resolve to false/dismissed=true when the Web Share API is absent (jsdom,
22
+ // unsupported browsers) or the user cancels. canShare returns false when the API is absent or the
23
+ // platform cannot share the given content. isAvailable returns true when navigator.share exists.
24
+ // Files are converted from ShareFile (data URL) to DOM File at the boundary; conversion errors
25
+ // fall back to false.
26
+ export function createWebShareBackend() {
27
+ return {
28
+ isAvailable() {
29
+ return typeof navigator !== 'undefined' && 'share' in navigator;
30
+ },
31
+ canShare(content) {
32
+ if (typeof navigator === 'undefined' || !('share' in navigator))
33
+ return false;
34
+ try {
35
+ const data = shareContentToNavigatorData(content);
36
+ return navigator.canShare?.(data) ?? false;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ },
42
+ async share(content, _options) {
43
+ if (typeof navigator === 'undefined' || !('share' in navigator) || typeof navigator.share !== 'function') {
44
+ return false;
45
+ }
46
+ try {
47
+ const data = shareContentToNavigatorData(content);
48
+ await navigator.share(data);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ },
55
+ async shareWithResult(content, _options) {
56
+ if (typeof navigator === 'undefined' || !('share' in navigator) || typeof navigator.share !== 'function') {
57
+ return { completed: false, activityType: null, dismissed: false };
58
+ }
59
+ try {
60
+ const data = shareContentToNavigatorData(content);
61
+ await navigator.share(data);
62
+ return { completed: true, activityType: null, dismissed: false };
63
+ }
64
+ catch (err) {
65
+ // AbortError means the user explicitly cancelled; other errors are failures.
66
+ const dismissed = err instanceof Error && err.name === 'AbortError';
67
+ return { completed: false, activityType: null, dismissed };
68
+ }
69
+ },
70
+ };
71
+ }
72
+ // Stops delivery to `signals` and forgets its subscription. Safe to call when not attached.
73
+ export function detachShareSignals(signals) {
74
+ _signalListeners.delete(signals);
75
+ }
76
+ // Releases `signals` for garbage collection by detaching its subscription. The signals remain plain
77
+ // GC-managed memory afterward.
78
+ export function disposeShareSignals(signals) {
79
+ detachShareSignals(signals);
80
+ }
81
+ // Enables a signals group for share result events. Signals stay inert until attachShareSignals is
82
+ // called. This is the opt-in; the cost is assumed when attached.
83
+ export function enableShareSignals() {
84
+ return {
85
+ onShareResult: createSignal(),
86
+ };
87
+ }
88
+ // The active share backend, or a lazily-created web default. There is always a backend.
89
+ export function getShareBackend() {
90
+ if (_backend === null)
91
+ _backend = createWebShareBackend();
92
+ return _backend;
93
+ }
94
+ // True when content has at least one populated field (title, text, url, or a non-empty files array).
95
+ // The Web Share API requires at least one field to be present; calling shareContent with an empty
96
+ // payload will throw on some engines, which shareContent swallows to false. Use this to detect
97
+ // an obviously-empty payload before calling shareContent.
98
+ export function hasShareContentFields(content) {
99
+ if (content.title !== undefined && content.title !== '')
100
+ return true;
101
+ if (content.text !== undefined && content.text !== '')
102
+ return true;
103
+ if (content.url !== undefined && content.url !== '')
104
+ return true;
105
+ if (content.files !== undefined && content.files.length > 0)
106
+ return true;
107
+ return false;
108
+ }
109
+ // True when the active backend's platform supports sharing at all (capability-level probe,
110
+ // independent of any content). Distinct from canShareContent: this asks "can this platform share?"
111
+ // while canShareContent asks "is this specific content shareable?".
112
+ export function isShareAvailable() {
113
+ return getShareBackend().isAvailable();
114
+ }
115
+ // Installs a native host share backend; pass null to fall back to the web default.
116
+ export function setShareBackend(backend) {
117
+ _backend = backend;
118
+ }
119
+ // Opens the native share sheet with the given content. Resolves true on success, false when the host
120
+ // denies, the user cancels, or sharing is unavailable. An empty content payload (no title/text/url/
121
+ // files) is caught by hasShareContentFields and returns false immediately rather than forwarding to the
122
+ // backend (which may throw). Pass options to control presentation on native hosts (parentWindow,
123
+ // sourceRect on iPad).
124
+ export function shareContent(content, options) {
125
+ if (!hasShareContentFields(content))
126
+ return Promise.resolve(false);
127
+ return getShareBackend().share(content, options);
128
+ }
129
+ // Opens the native share sheet and returns a full ShareResult describing completion, cancellation,
130
+ // and which activity/app was chosen. Emits onShareResult on all attached ShareSignals groups.
131
+ // An empty content payload returns { completed: false, activityType: null, dismissed: false }
132
+ // immediately. Pass options to control presentation on native hosts.
133
+ export async function shareContentWithResult(content, options) {
134
+ if (!hasShareContentFields(content)) {
135
+ return { completed: false, activityType: null, dismissed: false };
136
+ }
137
+ const result = await getShareBackend().shareWithResult(content, options);
138
+ if (_signalListeners.size > 0) {
139
+ for (const signals of _signalListeners.keys()) {
140
+ emitSignal(signals.onShareResult, result);
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+ // Opens the share sheet with a plain text payload. A convenience wrapper over shareContent.
146
+ export function shareText(text, options) {
147
+ return shareContent({ text }, options);
148
+ }
149
+ // Opens the share sheet with a URL payload. A convenience wrapper over shareContent.
150
+ export function shareUrl(url, options) {
151
+ return shareContent({ url }, options);
152
+ }
153
+ let _backend = null;
154
+ // Maps signals groups that have been attached via attachShareSignals.
155
+ const _signalListeners = new Map();
156
+ // Converts a ShareContent (with portable ShareFile descriptors) to the navigator.share data shape,
157
+ // converting ShareFile data URLs to DOM File objects at the web boundary.
158
+ function shareContentToNavigatorData(content) {
159
+ const data = {};
160
+ if (content.title !== undefined)
161
+ data.title = content.title;
162
+ if (content.text !== undefined)
163
+ data.text = content.text;
164
+ if (content.url !== undefined)
165
+ data.url = content.url;
166
+ if (content.files !== undefined && content.files.length > 0) {
167
+ data.files = content.files.map((f) => shareFileToDomFile(f));
168
+ }
169
+ return data;
170
+ }
171
+ // Converts a portable ShareFile descriptor to a DOM File for navigator.share / navigator.canShare.
172
+ function shareFileToDomFile(file) {
173
+ // Parse the data URL: 'data:<mimeType>;base64,<data>' or 'data:<mimeType>,<data>'
174
+ const comma = file.dataUrl.indexOf(',');
175
+ const header = file.dataUrl.substring(0, comma);
176
+ const body = file.dataUrl.substring(comma + 1);
177
+ const isBase64 = header.includes(';base64');
178
+ let bytes;
179
+ if (isBase64) {
180
+ const binary = atob(body);
181
+ bytes = new Uint8Array(new ArrayBuffer(binary.length));
182
+ for (let i = 0; i < binary.length; i++) {
183
+ bytes[i] = binary.charCodeAt(i);
184
+ }
185
+ }
186
+ else {
187
+ const decoded = decodeURIComponent(body);
188
+ const encoded = new TextEncoder().encode(decoded);
189
+ bytes = new Uint8Array(new ArrayBuffer(encoded.length));
190
+ bytes.set(encoded);
191
+ }
192
+ return new File([bytes], file.name, { type: file.mimeType });
193
+ }
194
+ //# sourceMappingURL=share.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/share",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Native share sheet (title, text, url, files) over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,460 @@
1
+ import { connectSignal } from '@flighthq/signals';
2
+ import type { ShareBackend, ShareContent, ShareFile, ShareOptions, ShareResult } from '@flighthq/types';
3
+
4
+ import {
5
+ attachShareSignals,
6
+ canShareContent,
7
+ createWebShareBackend,
8
+ detachShareSignals,
9
+ disposeShareSignals,
10
+ enableShareSignals,
11
+ getShareBackend,
12
+ hasShareContentFields,
13
+ isShareAvailable,
14
+ setShareBackend,
15
+ shareContent,
16
+ shareContentWithResult,
17
+ shareText,
18
+ shareUrl,
19
+ } from './share';
20
+
21
+ function fakeBackend(
22
+ overrides: Partial<ShareBackend & { shared: ShareContent | null }> = {},
23
+ ): ShareBackend & { shared: ShareContent | null } {
24
+ return {
25
+ shared: null,
26
+ isAvailable() {
27
+ return true;
28
+ },
29
+ async share(content) {
30
+ (this as { shared: ShareContent | null }).shared = content;
31
+ return true;
32
+ },
33
+ async shareWithResult(content) {
34
+ (this as { shared: ShareContent | null }).shared = content;
35
+ return { completed: true, activityType: null, dismissed: false };
36
+ },
37
+ canShare() {
38
+ return true;
39
+ },
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ afterEach(() => setShareBackend(null));
45
+
46
+ describe('attachShareSignals', () => {
47
+ it('replaces an existing attachment idempotently', async () => {
48
+ const signals = enableShareSignals();
49
+ attachShareSignals(signals);
50
+ attachShareSignals(signals); // second attach should not double-fire
51
+ let count = 0;
52
+ connectSignal(signals.onShareResult, () => count++);
53
+ setShareBackend(fakeBackend());
54
+ await shareContentWithResult({ text: 'hi' });
55
+ expect(count).toBe(1);
56
+ detachShareSignals(signals);
57
+ });
58
+
59
+ it('emits onShareResult when shareContentWithResult is called after attach', async () => {
60
+ const signals = enableShareSignals();
61
+ attachShareSignals(signals);
62
+ const results: ShareResult[] = [];
63
+ connectSignal(signals.onShareResult, (r) => results.push(r));
64
+ setShareBackend(fakeBackend());
65
+ await shareContentWithResult({ text: 'hello' });
66
+ expect(results).toHaveLength(1);
67
+ expect(results[0]!.completed).toBe(true);
68
+ detachShareSignals(signals);
69
+ });
70
+
71
+ it('does not emit after detach', async () => {
72
+ const signals = enableShareSignals();
73
+ attachShareSignals(signals);
74
+ detachShareSignals(signals);
75
+ let count = 0;
76
+ connectSignal(signals.onShareResult, () => count++);
77
+ setShareBackend(fakeBackend());
78
+ await shareContentWithResult({ text: 'hi' });
79
+ expect(count).toBe(0);
80
+ });
81
+ });
82
+
83
+ describe('canShareContent', () => {
84
+ it('reflects the backend result', () => {
85
+ setShareBackend(fakeBackend());
86
+ expect(canShareContent({ text: 'x' })).toBe(true);
87
+ });
88
+
89
+ it('returns false from the web backend in jsdom without throwing', () => {
90
+ expect(canShareContent({ text: 'x' })).toBe(false);
91
+ });
92
+
93
+ it('returns false when files cannot be shared', () => {
94
+ setShareBackend({
95
+ isAvailable: () => true,
96
+ canShare: () => false,
97
+ share: async () => false,
98
+ shareWithResult: async () => ({ completed: false, activityType: null, dismissed: false }),
99
+ });
100
+ const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,abc' };
101
+ expect(canShareContent({ files: [file] })).toBe(false);
102
+ });
103
+
104
+ it('returns true when files can be shared', () => {
105
+ setShareBackend(fakeBackend());
106
+ const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,abc' };
107
+ expect(canShareContent({ files: [file] })).toBe(true);
108
+ });
109
+ });
110
+
111
+ describe('createWebShareBackend', () => {
112
+ it('returns false for share/canShare and isAvailable=false when the API is absent', async () => {
113
+ const backend = createWebShareBackend();
114
+ expect(backend.isAvailable()).toBe(false);
115
+ expect(await backend.share({ text: 'x' })).toBe(false);
116
+ expect(backend.canShare({ text: 'x' })).toBe(false);
117
+ });
118
+
119
+ it('shareWithResult returns completed=false dismissed=false when API is absent', async () => {
120
+ const backend = createWebShareBackend();
121
+ const result = await backend.shareWithResult({ text: 'x' });
122
+ expect(result.completed).toBe(false);
123
+ expect(result.dismissed).toBe(false);
124
+ expect(result.activityType).toBeNull();
125
+ });
126
+
127
+ it('shareWithResult returns dismissed=true on AbortError', async () => {
128
+ const backend = createWebShareBackend();
129
+ Object.defineProperty(globalThis, 'navigator', {
130
+ value: {
131
+ share: async () => {
132
+ const err = new Error('User aborted');
133
+ err.name = 'AbortError';
134
+ throw err;
135
+ },
136
+ canShare: () => true,
137
+ },
138
+ configurable: true,
139
+ });
140
+ const result = await backend.shareWithResult({ text: 'hi' });
141
+ expect(result.completed).toBe(false);
142
+ expect(result.dismissed).toBe(true);
143
+ Object.defineProperty(globalThis, 'navigator', { value: undefined, configurable: true });
144
+ });
145
+
146
+ it('shareWithResult returns dismissed=false on non-AbortError', async () => {
147
+ const backend = createWebShareBackend();
148
+ Object.defineProperty(globalThis, 'navigator', {
149
+ value: {
150
+ share: async () => {
151
+ const err = new Error('Data error');
152
+ err.name = 'DataError';
153
+ throw err;
154
+ },
155
+ canShare: () => true,
156
+ },
157
+ configurable: true,
158
+ });
159
+ const result = await backend.shareWithResult({ text: 'hi' });
160
+ expect(result.completed).toBe(false);
161
+ expect(result.dismissed).toBe(false);
162
+ Object.defineProperty(globalThis, 'navigator', { value: undefined, configurable: true });
163
+ });
164
+
165
+ it('isAvailable returns true when navigator.share exists', () => {
166
+ Object.defineProperty(globalThis, 'navigator', {
167
+ value: { share: async () => {} },
168
+ configurable: true,
169
+ });
170
+ const backend = createWebShareBackend();
171
+ expect(backend.isAvailable()).toBe(true);
172
+ Object.defineProperty(globalThis, 'navigator', { value: undefined, configurable: true });
173
+ });
174
+
175
+ it('canShare delegates files to navigator.canShare', () => {
176
+ let passedData: ShareData | undefined;
177
+ Object.defineProperty(globalThis, 'navigator', {
178
+ value: {
179
+ share: async () => {},
180
+ canShare: (data: ShareData) => {
181
+ passedData = data;
182
+ return true;
183
+ },
184
+ },
185
+ configurable: true,
186
+ });
187
+ const backend = createWebShareBackend();
188
+ const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,AA==' };
189
+ backend.canShare({ files: [file] });
190
+ expect(passedData?.files).toHaveLength(1);
191
+ expect(passedData?.files?.[0]).toBeInstanceOf(File);
192
+ Object.defineProperty(globalThis, 'navigator', { value: undefined, configurable: true });
193
+ });
194
+ });
195
+
196
+ describe('detachShareSignals', () => {
197
+ it('is safe to call when not attached', () => {
198
+ const signals = enableShareSignals();
199
+ expect(() => detachShareSignals(signals)).not.toThrow();
200
+ });
201
+ });
202
+
203
+ describe('disposeShareSignals', () => {
204
+ it('releases signal group without throwing', () => {
205
+ const signals = enableShareSignals();
206
+ attachShareSignals(signals);
207
+ expect(() => disposeShareSignals(signals)).not.toThrow();
208
+ });
209
+ });
210
+
211
+ describe('enableShareSignals', () => {
212
+ it('returns a signals group with onShareResult signal', () => {
213
+ const signals = enableShareSignals();
214
+ expect(signals.onShareResult).toBeDefined();
215
+ });
216
+ });
217
+
218
+ describe('getShareBackend', () => {
219
+ it('falls back to a web backend', () => {
220
+ expect(getShareBackend()).not.toBeNull();
221
+ });
222
+
223
+ it('returns the registered backend', () => {
224
+ const backend = fakeBackend();
225
+ setShareBackend(backend);
226
+ expect(getShareBackend()).toBe(backend);
227
+ });
228
+ });
229
+
230
+ describe('hasShareContentFields', () => {
231
+ it('returns false for empty object', () => {
232
+ expect(hasShareContentFields({})).toBe(false);
233
+ });
234
+
235
+ it('returns false for empty strings', () => {
236
+ expect(hasShareContentFields({ title: '', text: '', url: '' })).toBe(false);
237
+ });
238
+
239
+ it('returns false for empty files array', () => {
240
+ expect(hasShareContentFields({ files: [] })).toBe(false);
241
+ });
242
+
243
+ it('returns true for non-empty title', () => {
244
+ expect(hasShareContentFields({ title: 'Hello' })).toBe(true);
245
+ });
246
+
247
+ it('returns true for non-empty text', () => {
248
+ expect(hasShareContentFields({ text: 'x' })).toBe(true);
249
+ });
250
+
251
+ it('returns true for non-empty url', () => {
252
+ expect(hasShareContentFields({ url: 'https://example.com' })).toBe(true);
253
+ });
254
+
255
+ it('returns true for non-empty files array', () => {
256
+ const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,AA==' };
257
+ expect(hasShareContentFields({ files: [file] })).toBe(true);
258
+ });
259
+ });
260
+
261
+ describe('isShareAvailable', () => {
262
+ it('returns false from the web backend in jsdom', () => {
263
+ expect(isShareAvailable()).toBe(false);
264
+ });
265
+
266
+ it('returns true when the backend reports available', () => {
267
+ setShareBackend(fakeBackend());
268
+ expect(isShareAvailable()).toBe(true);
269
+ });
270
+
271
+ it('returns true when navigator.share exists', () => {
272
+ Object.defineProperty(globalThis, 'navigator', {
273
+ value: { share: async () => {} },
274
+ configurable: true,
275
+ });
276
+ expect(isShareAvailable()).toBe(true);
277
+ Object.defineProperty(globalThis, 'navigator', { value: undefined, configurable: true });
278
+ });
279
+ });
280
+
281
+ describe('setShareBackend', () => {
282
+ it('clears back to the web fallback when passed null', () => {
283
+ setShareBackend(fakeBackend());
284
+ setShareBackend(null);
285
+ expect(getShareBackend()).not.toBeNull();
286
+ });
287
+ });
288
+
289
+ describe('shareContent', () => {
290
+ it('shares via the active backend', async () => {
291
+ const backend = fakeBackend();
292
+ setShareBackend(backend);
293
+ expect(await shareContent({ title: 't', url: 'u' })).toBe(true);
294
+ expect(backend.shared).toEqual({ title: 't', url: 'u' });
295
+ });
296
+
297
+ it('returns false from the web backend in jsdom without throwing', async () => {
298
+ expect(await shareContent({ text: 'x' })).toBe(false);
299
+ });
300
+
301
+ it('returns false immediately for empty content without calling backend', async () => {
302
+ let called = false;
303
+ setShareBackend({
304
+ isAvailable: () => true,
305
+ canShare: () => false,
306
+ share: async () => {
307
+ called = true;
308
+ return true;
309
+ },
310
+ shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
311
+ });
312
+ expect(await shareContent({})).toBe(false);
313
+ expect(called).toBe(false);
314
+ });
315
+
316
+ it('passes options to backend', async () => {
317
+ let passedOptions: ShareOptions | undefined;
318
+ setShareBackend({
319
+ isAvailable: () => true,
320
+ canShare: () => true,
321
+ share: async (_content, options) => {
322
+ passedOptions = options;
323
+ return true;
324
+ },
325
+ shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
326
+ });
327
+ const opts: ShareOptions = { chooserTitle: 'Share via' };
328
+ await shareContent({ text: 'hi' }, opts);
329
+ expect(passedOptions).toEqual(opts);
330
+ });
331
+
332
+ it('shares files', async () => {
333
+ const backend = fakeBackend();
334
+ setShareBackend(backend);
335
+ const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,AA==' };
336
+ expect(await shareContent({ files: [file] })).toBe(true);
337
+ expect(backend.shared).toEqual({ files: [file] });
338
+ });
339
+ });
340
+
341
+ describe('shareContentWithResult', () => {
342
+ it('returns completed result from backend', async () => {
343
+ setShareBackend(fakeBackend());
344
+ const result = await shareContentWithResult({ text: 'hello' });
345
+ expect(result.completed).toBe(true);
346
+ expect(result.dismissed).toBe(false);
347
+ expect(result.activityType).toBeNull();
348
+ });
349
+
350
+ it('returns empty result for empty content without calling backend', async () => {
351
+ let called = false;
352
+ setShareBackend({
353
+ isAvailable: () => true,
354
+ canShare: () => false,
355
+ share: async () => true,
356
+ shareWithResult: async () => {
357
+ called = true;
358
+ return { completed: true, activityType: null, dismissed: false };
359
+ },
360
+ });
361
+ const result = await shareContentWithResult({});
362
+ expect(result.completed).toBe(false);
363
+ expect(called).toBe(false);
364
+ });
365
+
366
+ it('emits onShareResult to attached signal groups', async () => {
367
+ setShareBackend(fakeBackend());
368
+ const signals = enableShareSignals();
369
+ attachShareSignals(signals);
370
+ const results: ShareResult[] = [];
371
+ connectSignal(signals.onShareResult, (r) => results.push(r));
372
+ await shareContentWithResult({ url: 'https://example.com' });
373
+ expect(results).toHaveLength(1);
374
+ expect(results[0]!.completed).toBe(true);
375
+ detachShareSignals(signals);
376
+ });
377
+
378
+ it('does not emit to detached signal groups', async () => {
379
+ setShareBackend(fakeBackend());
380
+ const signals = enableShareSignals();
381
+ attachShareSignals(signals);
382
+ detachShareSignals(signals);
383
+ let count = 0;
384
+ connectSignal(signals.onShareResult, () => count++);
385
+ await shareContentWithResult({ text: 'hi' });
386
+ expect(count).toBe(0);
387
+ });
388
+
389
+ it('passes options to backend', async () => {
390
+ let passedOptions: ShareOptions | undefined;
391
+ setShareBackend({
392
+ isAvailable: () => true,
393
+ canShare: () => true,
394
+ share: async () => true,
395
+ shareWithResult: async (_content, options) => {
396
+ passedOptions = options;
397
+ return { completed: true, activityType: 'com.example.App', dismissed: false };
398
+ },
399
+ });
400
+ const opts: ShareOptions = { excludedActivityTypes: ['com.apple.UIKit.activity.Print'] };
401
+ const result = await shareContentWithResult({ text: 'hi' }, opts);
402
+ expect(passedOptions).toEqual(opts);
403
+ expect(result.activityType).toBe('com.example.App');
404
+ });
405
+
406
+ it('returns false from web backend in jsdom without throwing', async () => {
407
+ const result = await shareContentWithResult({ text: 'x' });
408
+ expect(result.completed).toBe(false);
409
+ });
410
+ });
411
+
412
+ describe('shareText', () => {
413
+ it('shares via shareContent with text payload', async () => {
414
+ const backend = fakeBackend();
415
+ setShareBackend(backend);
416
+ expect(await shareText('hello world')).toBe(true);
417
+ expect(backend.shared).toEqual({ text: 'hello world' });
418
+ });
419
+
420
+ it('passes options to shareContent', async () => {
421
+ let passedOptions: ShareOptions | undefined;
422
+ setShareBackend({
423
+ isAvailable: () => true,
424
+ canShare: () => true,
425
+ share: async (_content, options) => {
426
+ passedOptions = options;
427
+ return true;
428
+ },
429
+ shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
430
+ });
431
+ const opts: ShareOptions = { chooserTitle: 'Share text' };
432
+ await shareText('hi', opts);
433
+ expect(passedOptions).toEqual(opts);
434
+ });
435
+ });
436
+
437
+ describe('shareUrl', () => {
438
+ it('shares via shareContent with url payload', async () => {
439
+ const backend = fakeBackend();
440
+ setShareBackend(backend);
441
+ expect(await shareUrl('https://example.com')).toBe(true);
442
+ expect(backend.shared).toEqual({ url: 'https://example.com' });
443
+ });
444
+
445
+ it('passes options to shareContent', async () => {
446
+ let passedOptions: ShareOptions | undefined;
447
+ setShareBackend({
448
+ isAvailable: () => true,
449
+ canShare: () => true,
450
+ share: async (_content, options) => {
451
+ passedOptions = options;
452
+ return true;
453
+ },
454
+ shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
455
+ });
456
+ const opts: ShareOptions = { chooserTitle: 'Share link' };
457
+ await shareUrl('https://example.com', opts);
458
+ expect(passedOptions).toEqual(opts);
459
+ });
460
+ });