@flighthq/share 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @flighthq/share
2
2
 
3
- Native share sheet (title, text, url, files) over a swappable web/native backend
3
+ Explicit host share-sheet commands for content and portable files
4
4
 
5
5
  ## Install
6
6
 
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/share`. The `@flight
13
13
  This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
14
 
15
15
  - [Flight project](https://github.com/flighthq/flight)
16
- - [Source for @flighthq/share@0.5.0-next.1944.cd6b6e7](https://github.com/flighthq/flight/tree/cd6b6e71eb472b3a4249830a7743333f019fa7b7/packages/share)
17
- - [License](https://github.com/flighthq/flight/blob/cd6b6e71eb472b3a4249830a7743333f019fa7b7/LICENSE.md)
16
+ - [Source for @flighthq/share@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/share)
17
+ - [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { explainShareBackend, attachShareSignals, canShareContent, detachShareSignals, disposeShareSignals, enableShareSignals, hasShareContentFields, isShareAvailable, shareContent, shareContentWithResult, shareFiles, shareText, shareUrl, } from './contract';
1
+ export { attachShareSignals, canShareContent, canShareFiles, detachShareSignals, disposeShareSignals, enableShareSignals, hasShareContentFields, shareContent, shareContentWithResult, shareFiles, shareText, shareUrl, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,SAAS,EACT,QAAQ,GACT,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,SAAS,EACT,QAAQ,GACT,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { explainShareBackend, attachShareSignals, canShareContent, detachShareSignals, disposeShareSignals, enableShareSignals, hasShareContentFields, isShareAvailable, shareContent, shareContentWithResult, shareFiles, shareText, shareUrl, } from './contract';
1
+ export { attachShareSignals, canShareContent, canShareFiles, detachShareSignals, disposeShareSignals, enableShareSignals, hasShareContentFields, shareContent, shareContentWithResult, shareFiles, shareText, shareUrl, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,SAAS,EACT,QAAQ,GACT,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,SAAS,EACT,QAAQ,GACT,MAAM,YAAY,CAAC"}
package/dist/share.d.ts CHANGED
@@ -1,22 +1,14 @@
1
- import type { BackendExplanation } from '@flighthq/types/contract';
2
- import type { ShareBackend, ShareContent, ShareFile, ShareOptions, ShareResult, ShareSignals } from '@flighthq/types/contract';
1
+ import type { HasShareContent, HasShareFiles, ShareContent, ShareFile, ShareResult, ShareSignals } from '@flighthq/types/contract';
3
2
  export declare function attachShareSignals(signals: ShareSignals): void;
4
- export declare function canShareContent(content: Readonly<ShareContent>): boolean;
5
- export declare function createWebShareBackend(): ShareBackend;
3
+ export declare function canShareContent(host: HasShareContent, content: Readonly<ShareContent>): boolean;
4
+ export declare function canShareFiles(host: HasShareFiles, files: readonly ShareFile[]): boolean;
6
5
  export declare function detachShareSignals(signals: ShareSignals): void;
7
6
  export declare function disposeShareSignals(signals: ShareSignals): void;
8
7
  export declare function enableShareSignals(): ShareSignals;
9
- export declare function explainShareBackend(): BackendExplanation;
10
- export declare function getShareBackend(): ShareBackend;
11
8
  export declare function hasShareContentFields(content: Readonly<ShareContent>): boolean;
12
- export declare function installShareHostBackend(backend: ShareBackend): void;
13
- export declare function isShareAvailable(): boolean;
14
- export declare function observeShareHostResult(operation: string, succeeded: boolean): void;
15
- export declare function resetShareBackendForTest(): void;
16
- export declare function setShareBackend(backend: ShareBackend | null): void;
17
- export declare function shareContent(content: Readonly<ShareContent>, options?: Readonly<ShareOptions>): Promise<boolean>;
18
- export declare function shareContentWithResult(content: Readonly<ShareContent>, options?: Readonly<ShareOptions>): Promise<ShareResult>;
19
- export declare function shareFiles(files: readonly ShareFile[], options?: Readonly<ShareOptions>): Promise<boolean>;
20
- export declare function shareText(text: string, options?: Readonly<ShareOptions>): Promise<boolean>;
21
- export declare function shareUrl(url: string, options?: Readonly<ShareOptions>): Promise<boolean>;
9
+ export declare function shareContent(host: HasShareContent, content: Readonly<ShareContent>): Promise<boolean>;
10
+ export declare function shareContentWithResult(host: HasShareContent, content: Readonly<ShareContent>): Promise<ShareResult>;
11
+ export declare function shareFiles(host: HasShareFiles, files: readonly ShareFile[]): Promise<boolean>;
12
+ export declare function shareText(host: HasShareContent, text: string): Promise<boolean>;
13
+ export declare function shareUrl(host: HasShareContent, url: string): Promise<boolean>;
22
14
  //# sourceMappingURL=share.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE9D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAExE;AAED,wBAAgB,qBAAqB,IAAI,YAAY,CA2CpD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE9D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAIjD;AAED,wBAAgB,mBAAmB,IAAI,kBAAkB,CAaxD;AAED,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAM9E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAMnE;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAKlF;AAED,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAElE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGhH;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC/B,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAC/B,OAAO,CAAC,WAAW,CAAC,CAStB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAE1G;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAE1F;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAExF"}
1
+ {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,SAAS,EAET,WAAW,EACX,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE9D;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAE/F;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAGvF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE9D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAG/D;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAK9E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGrG;AAED,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAC9B,OAAO,CAAC,WAAW,CAAC,CAStB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAI7F;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE/E;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE7E"}
package/dist/share.js CHANGED
@@ -1,82 +1,26 @@
1
- import { createSignal, emitSignal } from '@flighthq/signals/contract';
1
+ import { createEntity } from '@flighthq/entity/contract';
2
+ import { clearSignal, createSignal, emitSignal } from '@flighthq/signals/contract';
2
3
  export function attachShareSignals(signals) {
3
4
  _attachedSignals.add(signals);
4
5
  }
5
- export function canShareContent(content) {
6
- return getShareBackend().canShare(content);
6
+ // This is payload validation within the content capability. Capability presence itself is expressed
7
+ // by HasShareContent, so a host without the slot is a type error rather than a false probe.
8
+ export function canShareContent(host, content) {
9
+ return hasShareContentFields(content) && host.share.content.canShareContent(content);
7
10
  }
8
- export function createWebShareBackend() {
9
- return {
10
- isAvailable() {
11
- return typeof navigator !== 'undefined' && 'share' in navigator;
12
- },
13
- canShare(content) {
14
- if (typeof navigator === 'undefined' || !('share' in navigator))
15
- return false;
16
- try {
17
- const data = shareContentToNavigatorData(content);
18
- return navigator.canShare?.(data) ?? false;
19
- }
20
- catch {
21
- return false;
22
- }
23
- },
24
- async share(content, _options) {
25
- if (typeof navigator === 'undefined' || !('share' in navigator) || typeof navigator.share !== 'function') {
26
- return false;
27
- }
28
- try {
29
- const data = shareContentToNavigatorData(content);
30
- await navigator.share(data);
31
- return true;
32
- }
33
- catch {
34
- return false;
35
- }
36
- },
37
- async shareWithResult(content, _options) {
38
- if (typeof navigator === 'undefined' || !('share' in navigator) || typeof navigator.share !== 'function') {
39
- return { completed: false, activityType: null, dismissed: false };
40
- }
41
- try {
42
- const data = shareContentToNavigatorData(content);
43
- await navigator.share(data);
44
- return { completed: true, activityType: null, dismissed: false };
45
- }
46
- catch (err) {
47
- const dismissed = err instanceof Error && err.name === 'AbortError';
48
- return { completed: false, activityType: null, dismissed };
49
- }
50
- },
51
- };
11
+ export function canShareFiles(host, files) {
12
+ const content = filesContent(files);
13
+ return content !== null && host.share.files.canShareContent(content);
52
14
  }
53
15
  export function detachShareSignals(signals) {
54
16
  _attachedSignals.delete(signals);
55
17
  }
56
18
  export function disposeShareSignals(signals) {
57
19
  detachShareSignals(signals);
20
+ clearSignal(signals.onShareResult);
58
21
  }
59
22
  export function enableShareSignals() {
60
- return {
61
- onShareResult: createSignal(),
62
- };
63
- }
64
- export function explainShareBackend() {
65
- if (_custom !== null) {
66
- return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
67
- }
68
- if (_host !== null) {
69
- return {
70
- conflict: _hostConflict,
71
- layer: 'host',
72
- operation: _hostObservation !== null ? _hostObservation.operation : null,
73
- viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
74
- };
75
- }
76
- return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
77
- }
78
- export function getShareBackend() {
79
- return _custom ?? _host ?? _sentinel;
23
+ return createEntity({ onShareResult: createSignal() });
80
24
  }
81
25
  export function hasShareContentFields(content) {
82
26
  if (content.title !== undefined && content.title !== '')
@@ -85,113 +29,38 @@ export function hasShareContentFields(content) {
85
29
  return true;
86
30
  if (content.url !== undefined && content.url !== '')
87
31
  return true;
88
- if (content.files !== undefined && content.files.length > 0)
89
- return true;
90
32
  return false;
91
33
  }
92
- export function installShareHostBackend(backend) {
93
- if (_host !== null) {
94
- if (_host !== backend)
95
- _hostConflict = true;
96
- return;
97
- }
98
- _host = backend;
99
- }
100
- export function isShareAvailable() {
101
- return getShareBackend().isAvailable();
102
- }
103
- export function observeShareHostResult(operation, succeeded) {
104
- _hostObservation = {
105
- operation,
106
- viability: succeeded ? 'available' : 'runtime-api-unavailable',
107
- };
108
- }
109
- export function resetShareBackendForTest() {
110
- _custom = null;
111
- _host = null;
112
- _hostConflict = false;
113
- _hostObservation = null;
114
- }
115
- export function setShareBackend(backend) {
116
- _custom = backend;
117
- }
118
- export function shareContent(content, options) {
34
+ export function shareContent(host, content) {
119
35
  if (!hasShareContentFields(content))
120
36
  return Promise.resolve(false);
121
- return getShareBackend().share(content, options);
37
+ return host.share.content.shareContent(content);
122
38
  }
123
- export async function shareContentWithResult(content, options) {
39
+ export async function shareContentWithResult(host, content) {
124
40
  if (!hasShareContentFields(content)) {
125
41
  return { completed: false, activityType: null, dismissed: false };
126
42
  }
127
- const result = await getShareBackend().shareWithResult(content, options);
43
+ const result = await host.share.content.shareContentWithResult(content);
128
44
  for (const signals of _attachedSignals) {
129
45
  emitSignal(signals.onShareResult, result);
130
46
  }
131
47
  return result;
132
48
  }
133
- export function shareFiles(files, options) {
134
- return shareContent({ files }, options);
49
+ export function shareFiles(host, files) {
50
+ const content = filesContent(files);
51
+ if (content === null)
52
+ return Promise.resolve(false);
53
+ return host.share.files.shareContent(content);
135
54
  }
136
- export function shareText(text, options) {
137
- return shareContent({ text }, options);
55
+ export function shareText(host, text) {
56
+ return shareContent(host, { text });
138
57
  }
139
- export function shareUrl(url, options) {
140
- return shareContent({ url }, options);
58
+ export function shareUrl(host, url) {
59
+ return shareContent(host, { url });
141
60
  }
142
- let _custom = null;
143
- let _host = null;
144
- let _hostConflict = false;
145
- let _hostObservation = null;
146
- const _sentinel = {
147
- canShare() {
148
- return false;
149
- },
150
- isAvailable() {
151
- return false;
152
- },
153
- async share() {
154
- return false;
155
- },
156
- async shareWithResult() {
157
- return { completed: false, activityType: null, dismissed: false };
158
- },
159
- };
160
61
  const _attachedSignals = new Set();
161
- function shareContentToNavigatorData(content) {
162
- const data = {};
163
- if (content.title !== undefined)
164
- data.title = content.title;
165
- if (content.text !== undefined)
166
- data.text = content.text;
167
- if (content.url !== undefined)
168
- data.url = content.url;
169
- if (content.files !== undefined && content.files.length > 0) {
170
- data.files = content.files.map((f) => shareFileToDomFile(f));
171
- }
172
- return data;
173
- }
174
- function shareFileToDomFile(file) {
175
- const comma = file.dataUrl.indexOf(',');
176
- if (comma === -1)
177
- throw new Error('share: dataUrl is not a data URL (no comma)');
178
- const header = file.dataUrl.substring(0, comma);
179
- const body = file.dataUrl.substring(comma + 1);
180
- const isBase64 = header.includes(';base64');
181
- let bytes;
182
- if (isBase64) {
183
- const binary = atob(body);
184
- bytes = new Uint8Array(new ArrayBuffer(binary.length));
185
- for (let i = 0; i < binary.length; i++) {
186
- bytes[i] = binary.charCodeAt(i);
187
- }
188
- }
189
- else {
190
- const decoded = decodeURIComponent(body);
191
- const encoded = new TextEncoder().encode(decoded);
192
- bytes = new Uint8Array(new ArrayBuffer(encoded.length));
193
- bytes.set(encoded);
194
- }
195
- return new File([bytes], file.name, { type: file.mimeType });
62
+ function filesContent(files) {
63
+ const first = files[0];
64
+ return first === undefined ? null : { files: [first, ...files.slice(1)] };
196
65
  }
197
66
  //# sourceMappingURL=share.js.map
package/dist/share.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/share",
3
- "version": "0.5.0-next.1944.cd6b6e7",
3
+ "version": "0.5.1-edge.638.d4dbd4e",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,12 +38,13 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/signals": "0.5.0-next.1944.cd6b6e7",
42
- "@flighthq/types": "0.5.0-next.1944.cd6b6e7"
41
+ "@flighthq/entity": "0.5.1-edge.638.d4dbd4e",
42
+ "@flighthq/signals": "0.5.1-edge.638.d4dbd4e",
43
+ "@flighthq/types": "0.5.1-edge.638.d4dbd4e"
43
44
  },
44
45
  "devDependencies": {
45
46
  "typescript": "^5.3.0"
46
47
  },
47
- "description": "Native share sheet (title, text, url, files) over a swappable web/native backend",
48
+ "description": "Explicit host share-sheet commands for content and portable files",
48
49
  "sideEffects": false
49
50
  }
package/src/share.test.ts CHANGED
@@ -1,609 +1,195 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
1
2
  import { connectSignal } from '@flighthq/signals/contract';
2
- import type { ShareBackend, ShareContent, ShareFile, ShareOptions, ShareResult } from '@flighthq/types/contract';
3
-
3
+ import { EntityRuntimeKey } from '@flighthq/types/contract';
4
+ import type {
5
+ HasShareContent,
6
+ HasShareFiles,
7
+ ShareContent,
8
+ ShareContentBackend,
9
+ ShareFilesBackend,
10
+ ShareResult,
11
+ } from '@flighthq/types/contract';
12
+
13
+ import * as shareContract from './contract';
4
14
  import {
5
15
  attachShareSignals,
6
16
  canShareContent,
7
- createWebShareBackend,
17
+ canShareFiles,
8
18
  detachShareSignals,
9
19
  disposeShareSignals,
10
20
  enableShareSignals,
11
- getShareBackend,
12
21
  hasShareContentFields,
13
- installShareHostBackend,
14
- isShareAvailable,
15
- resetShareBackendForTest,
16
- setShareBackend,
17
22
  shareContent,
18
23
  shareContentWithResult,
19
24
  shareFiles,
20
25
  shareText,
21
26
  shareUrl,
22
- explainShareBackend,
23
- observeShareHostResult,
24
27
  } from './share';
25
28
 
26
- function fakeBackend(
27
- overrides: Partial<ShareBackend & { shared: ShareContent | null }> = {},
28
- ): ShareBackend & { shared: ShareContent | null } {
29
+ type ContentImplementation = Omit<ShareContentBackend, typeof EntityRuntimeKey>;
30
+ type FilesImplementation = Omit<ShareFilesBackend, typeof EntityRuntimeKey>;
31
+
32
+ function contentHost(overrides: Partial<ContentImplementation> = {}): HasShareContent {
29
33
  return {
30
- shared: null,
31
- isAvailable() {
32
- return true;
33
- },
34
- async share(content) {
35
- (this as { shared: ShareContent | null }).shared = content;
36
- return true;
37
- },
38
- async shareWithResult(content) {
39
- (this as { shared: ShareContent | null }).shared = content;
40
- return { completed: true, activityType: null, dismissed: false };
41
- },
42
- canShare() {
43
- return true;
34
+ share: {
35
+ content: createEntity({
36
+ canShareContent: () => true,
37
+ shareContent: async () => true,
38
+ shareContentWithResult: async () => ({ activityType: null, completed: true, dismissed: false }),
39
+ ...overrides,
40
+ } satisfies ContentImplementation),
44
41
  },
45
- ...overrides,
46
42
  };
47
43
  }
48
44
 
49
- const originalNavigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
50
-
51
- function mockNavigator(value: unknown): void {
52
- Object.defineProperty(globalThis, 'navigator', { value, configurable: true });
53
- }
54
-
55
- function restoreNavigator(): void {
56
- if (originalNavigatorDescriptor !== undefined) {
57
- Object.defineProperty(globalThis, 'navigator', originalNavigatorDescriptor);
58
- } else {
59
- delete (globalThis as { navigator?: unknown }).navigator;
60
- }
45
+ function filesHost(overrides: Partial<FilesImplementation> = {}): HasShareFiles {
46
+ return {
47
+ share: {
48
+ files: createEntity({
49
+ canShareContent: () => true,
50
+ shareContent: async () => true,
51
+ shareContentWithResult: async () => ({ activityType: null, completed: true, dismissed: false }),
52
+ ...overrides,
53
+ } satisfies FilesImplementation),
54
+ },
55
+ };
61
56
  }
62
57
 
63
- afterEach(() => {
64
- resetShareBackendForTest();
65
- restoreNavigator();
66
- });
58
+ const file = { dataUrl: 'data:text/plain;base64,QQ==', mimeType: 'text/plain', name: 'a.txt' };
67
59
 
68
60
  describe('attachShareSignals', () => {
69
- it('replaces an existing attachment idempotently', async () => {
70
- const signals = enableShareSignals();
71
- attachShareSignals(signals);
72
- attachShareSignals(signals); // second attach should not double-fire
73
- let count = 0;
74
- connectSignal(signals.onShareResult, () => count++);
75
- setShareBackend(fakeBackend());
76
- await shareContentWithResult({ text: 'hi' });
77
- expect(count).toBe(1);
78
- detachShareSignals(signals);
79
- });
80
-
81
- it('emits onShareResult when shareContentWithResult is called after attach', async () => {
61
+ it('enables delivery from the detailed core command', async () => {
62
+ const result: ShareResult = { activityType: 'mail', completed: true, dismissed: false };
63
+ const host = contentHost({ shareContentWithResult: async () => result });
82
64
  const signals = enableShareSignals();
65
+ const listener = vi.fn();
66
+ connectSignal(signals.onShareResult, listener);
83
67
  attachShareSignals(signals);
84
- const results: ShareResult[] = [];
85
- connectSignal(signals.onShareResult, (r) => results.push(r));
86
- setShareBackend(fakeBackend());
87
- await shareContentWithResult({ text: 'hello' });
88
- expect(results).toHaveLength(1);
89
- expect(results[0]!.completed).toBe(true);
90
- detachShareSignals(signals);
91
- });
92
68
 
93
- it('does not emit after detach', async () => {
94
- const signals = enableShareSignals();
95
- attachShareSignals(signals);
69
+ expect(await shareContentWithResult(host, { title: 'flight' })).toEqual(result);
70
+ expect(listener).toHaveBeenCalledWith(result);
96
71
  detachShareSignals(signals);
97
- let count = 0;
98
- connectSignal(signals.onShareResult, () => count++);
99
- setShareBackend(fakeBackend());
100
- await shareContentWithResult({ text: 'hi' });
101
- expect(count).toBe(0);
102
72
  });
103
73
  });
104
74
 
105
75
  describe('canShareContent', () => {
106
- it('reflects the backend result', () => {
107
- setShareBackend(fakeBackend());
108
- expect(canShareContent({ text: 'x' })).toBe(true);
109
- });
110
-
111
- it('returns false from the sentinel when no backend is installed', () => {
112
- expect(canShareContent({ text: 'x' })).toBe(false);
113
- });
114
-
115
- it('returns false when files cannot be shared', () => {
116
- setShareBackend({
117
- isAvailable: () => true,
118
- canShare: () => false,
119
- share: async () => false,
120
- shareWithResult: async () => ({ completed: false, activityType: null, dismissed: false }),
121
- });
122
- const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,abc' };
123
- expect(canShareContent({ files: [file] })).toBe(false);
124
- });
76
+ it('validates meaningful payloads through the selected content slot', () => {
77
+ const can = vi.fn(() => true);
78
+ const host = contentHost({ canShareContent: can });
79
+ expect(canShareContent(host, { text: 'hello' })).toBe(true);
80
+ expect(can).toHaveBeenCalledWith({ text: 'hello' });
125
81
 
126
- it('returns true when files can be shared', () => {
127
- setShareBackend(fakeBackend());
128
- const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,abc' };
129
- expect(canShareContent({ files: [file] })).toBe(true);
82
+ // @ts-expect-error an empty object is not a meaningful Share content payload
83
+ expect(canShareContent(contentHost(), {})).toBe(false);
130
84
  });
131
85
  });
132
86
 
133
- describe('createWebShareBackend', () => {
134
- it('returns false for share/canShare and isAvailable=false when the API is absent', async () => {
135
- const backend = createWebShareBackend();
136
- expect(backend.isAvailable()).toBe(false);
137
- expect(await backend.share({ text: 'x' })).toBe(false);
138
- expect(backend.canShare({ text: 'x' })).toBe(false);
139
- });
140
-
141
- it('shareWithResult returns completed=false dismissed=false when API is absent', async () => {
142
- const backend = createWebShareBackend();
143
- const result = await backend.shareWithResult({ text: 'x' });
144
- expect(result.completed).toBe(false);
145
- expect(result.dismissed).toBe(false);
146
- expect(result.activityType).toBeNull();
147
- });
148
-
149
- it('shareWithResult returns dismissed=true on AbortError', async () => {
150
- const backend = createWebShareBackend();
151
- mockNavigator({
152
- share: async () => {
153
- const err = new Error('User aborted');
154
- err.name = 'AbortError';
155
- throw err;
156
- },
157
- canShare: () => true,
158
- });
159
- const result = await backend.shareWithResult({ text: 'hi' });
160
- expect(result.completed).toBe(false);
161
- expect(result.dismissed).toBe(true);
162
- });
163
-
164
- it('shareWithResult returns dismissed=false on non-AbortError', async () => {
165
- const backend = createWebShareBackend();
166
- mockNavigator({
167
- share: async () => {
168
- const err = new Error('Data error');
169
- err.name = 'DataError';
170
- throw err;
171
- },
172
- canShare: () => true,
173
- });
174
- const result = await backend.shareWithResult({ text: 'hi' });
175
- expect(result.completed).toBe(false);
176
- expect(result.dismissed).toBe(false);
177
- });
178
-
179
- it('isAvailable returns true when navigator.share exists', () => {
180
- mockNavigator({ share: async () => {} });
181
- const backend = createWebShareBackend();
182
- expect(backend.isAvailable()).toBe(true);
183
- });
184
-
185
- it('canShare delegates files to navigator.canShare', () => {
186
- let passedData: ShareData | undefined;
187
- mockNavigator({
188
- share: async () => {},
189
- canShare: (data: ShareData) => {
190
- passedData = data;
191
- return true;
192
- },
193
- });
194
- const backend = createWebShareBackend();
195
- const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,AA==' };
196
- backend.canShare({ files: [file] });
197
- expect(passedData?.files).toHaveLength(1);
198
- expect(passedData?.files?.[0]).toBeInstanceOf(File);
87
+ describe('canShareFiles', () => {
88
+ it('validates through the files slot only for non-empty lists', () => {
89
+ const host = filesHost();
90
+ expect(canShareFiles(host, [file])).toBe(true);
91
+ expect(canShareFiles(host, [])).toBe(false);
199
92
  });
200
93
  });
201
94
 
202
95
  describe('detachShareSignals', () => {
203
- it('is safe to call when not attached', () => {
96
+ it('stops delivery without clearing the signal Entity', async () => {
204
97
  const signals = enableShareSignals();
205
- expect(() => detachShareSignals(signals)).not.toThrow();
98
+ const listener = vi.fn();
99
+ connectSignal(signals.onShareResult, listener);
100
+ attachShareSignals(signals);
101
+ detachShareSignals(signals);
102
+ await shareContentWithResult(contentHost(), { title: 'quiet' });
103
+ expect(listener).not.toHaveBeenCalled();
206
104
  });
207
105
  });
208
106
 
209
107
  describe('disposeShareSignals', () => {
210
- it('releases signal group without throwing', () => {
108
+ it('dispose detaches and clears listeners', async () => {
211
109
  const signals = enableShareSignals();
110
+ const listener = vi.fn();
111
+ connectSignal(signals.onShareResult, listener);
212
112
  attachShareSignals(signals);
213
- expect(() => disposeShareSignals(signals)).not.toThrow();
113
+ disposeShareSignals(signals);
114
+ await shareContentWithResult(contentHost(), { text: 'quiet' });
115
+ expect(listener).not.toHaveBeenCalled();
214
116
  });
215
117
  });
216
118
 
217
119
  describe('enableShareSignals', () => {
218
- it('returns a signals group with onShareResult signal', () => {
219
- const signals = enableShareSignals();
220
- expect(signals.onShareResult).toBeDefined();
221
- });
222
- });
223
-
224
- describe('explainShareBackend', () => {
225
- afterEach(() => resetShareBackendForTest());
226
-
227
- it('reports host-not-enabled when no backend is installed', () => {
228
- resetShareBackendForTest();
229
- const explanation = explainShareBackend();
230
- expect(explanation.layer).toBe('host-not-enabled');
231
- expect(explanation.conflict).toBe(false);
232
- expect(explanation.viability).toBe('unobserved');
233
- });
234
-
235
- it('reports custom layer when a custom backend is set', () => {
236
- setShareBackend(fakeBackend());
237
- expect(explainShareBackend().layer).toBe('custom');
238
- });
239
-
240
- it('reports host layer when a host backend is installed', () => {
241
- installShareHostBackend(fakeBackend());
242
- expect(explainShareBackend().layer).toBe('host');
243
- });
244
-
245
- it('reports conflict when two different host backends are installed', () => {
246
- installShareHostBackend(fakeBackend());
247
- installShareHostBackend(fakeBackend());
248
- expect(explainShareBackend().conflict).toBe(true);
249
- });
250
- });
251
-
252
- describe('getShareBackend', () => {
253
- it('falls back to the sentinel when no backend is installed', () => {
254
- expect(getShareBackend()).not.toBeNull();
255
- });
256
-
257
- it('returns the registered backend', () => {
258
- const backend = fakeBackend();
259
- setShareBackend(backend);
260
- expect(getShareBackend()).toBe(backend);
120
+ it('returns an Entity-composed signal group', () => {
121
+ expect(EntityRuntimeKey in enableShareSignals()).toBe(true);
261
122
  });
262
123
  });
263
124
 
264
125
  describe('hasShareContentFields', () => {
265
- it('returns false for empty object', () => {
266
- expect(hasShareContentFields({})).toBe(false);
267
- });
268
-
269
- it('returns false for empty strings', () => {
270
- expect(hasShareContentFields({ title: '', text: '', url: '' })).toBe(false);
271
- });
272
-
273
- it('returns false for empty files array', () => {
274
- expect(hasShareContentFields({ files: [] })).toBe(false);
275
- });
276
-
277
- it('returns true for non-empty title', () => {
278
- expect(hasShareContentFields({ title: 'Hello' })).toBe(true);
279
- });
280
-
281
- it('returns true for non-empty text', () => {
282
- expect(hasShareContentFields({ text: 'x' })).toBe(true);
283
- });
284
-
285
- it('returns true for non-empty url', () => {
286
- expect(hasShareContentFields({ url: 'https://example.com' })).toBe(true);
287
- });
288
-
289
- it('returns true for non-empty files array', () => {
290
- const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,AA==' };
291
- expect(hasShareContentFields({ files: [file] })).toBe(true);
292
- });
293
- });
294
-
295
- describe('installShareHostBackend', () => {
296
- afterEach(() => resetShareBackendForTest());
297
-
298
- it('installs a host backend that getShareBackend returns', () => {
299
- const backend = fakeBackend();
300
- installShareHostBackend(backend);
301
- expect(getShareBackend()).toBe(backend);
302
- });
303
-
304
- it('is first-host-wins: a second different backend sets conflict', () => {
305
- const first = fakeBackend();
306
- const second = fakeBackend();
307
- installShareHostBackend(first);
308
- installShareHostBackend(second);
309
- expect(getShareBackend()).toBe(first);
310
- expect(explainShareBackend().conflict).toBe(true);
311
- });
312
- });
313
-
314
- describe('isShareAvailable', () => {
315
- it('returns false from the sentinel when no backend is installed', () => {
316
- expect(isShareAvailable()).toBe(false);
317
- });
318
-
319
- it('returns true when the backend reports available', () => {
320
- setShareBackend(fakeBackend());
321
- expect(isShareAvailable()).toBe(true);
322
- });
323
-
324
- it('returns true when the web backend is installed and navigator.share exists', () => {
325
- mockNavigator({ share: async () => {} });
326
- installShareHostBackend(createWebShareBackend());
327
- expect(isShareAvailable()).toBe(true);
328
- });
329
- });
330
-
331
- describe('observeShareHostResult', () => {
332
- afterEach(() => resetShareBackendForTest());
333
-
334
- it('records a successful observation', () => {
335
- installShareHostBackend(fakeBackend());
336
- observeShareHostResult('canShare', true);
337
- const explanation = explainShareBackend();
338
- expect(explanation.operation).toBe('canShare');
339
- expect(explanation.viability).toBe('available');
340
- });
341
-
342
- it('records a failed observation', () => {
343
- installShareHostBackend(fakeBackend());
344
- observeShareHostResult('canShare', false);
345
- expect(explainShareBackend().viability).toBe('runtime-api-unavailable');
346
- });
347
- });
348
-
349
- describe('resetShareBackendForTest', () => {
350
- it('clears all backend slots', () => {
351
- setShareBackend(fakeBackend());
352
- installShareHostBackend(fakeBackend());
353
- observeShareHostResult('canShare', true);
354
- resetShareBackendForTest();
355
- expect(explainShareBackend().layer).toBe('host-not-enabled');
356
- expect(explainShareBackend().conflict).toBe(false);
357
- expect(explainShareBackend().viability).toBe('unobserved');
358
- });
359
- });
360
-
361
- describe('setShareBackend', () => {
362
- it('clears back to the sentinel when passed null', () => {
363
- setShareBackend(fakeBackend());
364
- setShareBackend(null);
365
- expect(getShareBackend()).not.toBeNull();
126
+ it.each<ShareContent>([{ title: 'T' }, { text: 'T' }, { url: 'https://flight.dev' }])(
127
+ 'accepts a meaningful content vector',
128
+ (content) => expect(hasShareContentFields(content)).toBe(true),
129
+ );
130
+ });
131
+
132
+ describe('Share contract surface', () => {
133
+ it('contains only explicit-host commands, payload validation, and core signal lifecycle', () => {
134
+ expect(Object.keys(shareContract).sort()).toEqual([
135
+ 'attachShareSignals',
136
+ 'canShareContent',
137
+ 'canShareFiles',
138
+ 'detachShareSignals',
139
+ 'disposeShareSignals',
140
+ 'enableShareSignals',
141
+ 'hasShareContentFields',
142
+ 'shareContent',
143
+ 'shareContentWithResult',
144
+ 'shareFiles',
145
+ 'shareText',
146
+ 'shareUrl',
147
+ ]);
366
148
  });
367
149
  });
368
150
 
369
151
  describe('shareContent', () => {
370
- it('shares via the active backend', async () => {
371
- const backend = fakeBackend();
372
- setShareBackend(backend);
373
- expect(await shareContent({ title: 't', url: 'u' })).toBe(true);
374
- expect(backend.shared).toEqual({ title: 't', url: 'u' });
375
- });
376
-
377
- it('returns false from the sentinel when no backend is installed', async () => {
378
- expect(await shareContent({ text: 'x' })).toBe(false);
379
- });
380
-
381
- it('returns false immediately for empty content without calling backend', async () => {
382
- let called = false;
383
- setShareBackend({
384
- isAvailable: () => true,
385
- canShare: () => false,
386
- share: async () => {
387
- called = true;
388
- return true;
389
- },
390
- shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
391
- });
392
- expect(await shareContent({})).toBe(false);
393
- expect(called).toBe(false);
394
- });
395
-
396
- it('passes options to backend', async () => {
397
- let passedOptions: ShareOptions | undefined;
398
- setShareBackend({
399
- isAvailable: () => true,
400
- canShare: () => true,
401
- share: async (_content, options) => {
402
- passedOptions = options;
403
- return true;
404
- },
405
- shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
406
- });
407
- const opts: ShareOptions = { chooserTitle: 'Share via' };
408
- await shareContent({ text: 'hi' }, opts);
409
- expect(passedOptions).toEqual(opts);
410
- });
411
-
412
- it('shares files', async () => {
413
- const backend = fakeBackend();
414
- setShareBackend(backend);
415
- const file: ShareFile = { name: 'img.png', mimeType: 'image/png', dataUrl: 'data:image/png;base64,AA==' };
416
- expect(await shareContent({ files: [file] })).toBe(true);
417
- expect(backend.shared).toEqual({ files: [file] });
152
+ it('rejects declared-but-empty content before dispatch', async () => {
153
+ const backend = vi.fn(async () => true);
154
+ const host = contentHost({ shareContent: backend });
155
+ const empty = { text: '' } as ShareContent;
156
+ expect(canShareContent(host, empty)).toBe(false);
157
+ expect(await shareContent(host, empty)).toBe(false);
158
+ expect(backend).not.toHaveBeenCalled();
418
159
  });
419
160
  });
420
161
 
421
162
  describe('shareContentWithResult', () => {
422
- it('returns completed result from backend', async () => {
423
- setShareBackend(fakeBackend());
424
- const result = await shareContentWithResult({ text: 'hello' });
425
- expect(result.completed).toBe(true);
426
- expect(result.dismissed).toBe(false);
427
- expect(result.activityType).toBeNull();
428
- });
429
-
430
- it('returns empty result for empty content without calling backend', async () => {
431
- let called = false;
432
- setShareBackend({
433
- isAvailable: () => true,
434
- canShare: () => false,
435
- share: async () => true,
436
- shareWithResult: async () => {
437
- called = true;
438
- return { completed: true, activityType: null, dismissed: false };
439
- },
440
- });
441
- const result = await shareContentWithResult({});
442
- expect(result.completed).toBe(false);
443
- expect(called).toBe(false);
444
- });
445
-
446
- it('emits onShareResult to attached signal groups', async () => {
447
- setShareBackend(fakeBackend());
448
- const signals = enableShareSignals();
449
- attachShareSignals(signals);
450
- const results: ShareResult[] = [];
451
- connectSignal(signals.onShareResult, (r) => results.push(r));
452
- await shareContentWithResult({ url: 'https://example.com' });
453
- expect(results).toHaveLength(1);
454
- expect(results[0]!.completed).toBe(true);
455
- detachShareSignals(signals);
456
- });
457
-
458
- it('does not emit to detached signal groups', async () => {
459
- setShareBackend(fakeBackend());
460
- const signals = enableShareSignals();
461
- attachShareSignals(signals);
462
- detachShareSignals(signals);
463
- let count = 0;
464
- connectSignal(signals.onShareResult, () => count++);
465
- await shareContentWithResult({ text: 'hi' });
466
- expect(count).toBe(0);
467
- });
468
-
469
- it('passes options to backend', async () => {
470
- let passedOptions: ShareOptions | undefined;
471
- setShareBackend({
472
- isAvailable: () => true,
473
- canShare: () => true,
474
- share: async () => true,
475
- shareWithResult: async (_content, options) => {
476
- passedOptions = options;
477
- return { completed: true, activityType: 'com.example.App', dismissed: false };
478
- },
479
- });
480
- const opts: ShareOptions = { excludedActivityTypes: ['com.apple.UIKit.activity.Print'] };
481
- const result = await shareContentWithResult({ text: 'hi' }, opts);
482
- expect(passedOptions).toEqual(opts);
483
- expect(result.activityType).toBe('com.example.App');
484
- });
485
-
486
- it('returns false from the sentinel when no backend is installed', async () => {
487
- const result = await shareContentWithResult({ text: 'x' });
488
- expect(result.completed).toBe(false);
163
+ it('returns a detailed provider outcome', async () => {
164
+ const result: ShareResult = { activityType: 'mail', completed: true, dismissed: false };
165
+ const host = contentHost({ shareContentWithResult: async () => result });
166
+ expect(await shareContentWithResult(host, { title: 'flight' })).toEqual(result);
489
167
  });
490
168
  });
491
169
 
492
170
  describe('shareFiles', () => {
493
- const file: ShareFile = { dataUrl: 'data:text/plain;base64,aGk=', mimeType: 'text/plain', name: 'hi.txt' };
494
-
495
- it('shares via shareContent with a files payload', async () => {
496
- const backend = fakeBackend();
497
- setShareBackend(backend);
498
- expect(await shareFiles([file])).toBe(true);
499
- expect(backend.shared).toEqual({ files: [file] });
500
- });
501
-
502
- it('passes options through', async () => {
503
- let passedOptions: ShareOptions | undefined;
504
- setShareBackend({
505
- isAvailable: () => true,
506
- canShare: () => true,
507
- share: async (_content, options) => {
508
- passedOptions = options;
509
- return true;
510
- },
511
- shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
512
- });
513
- const opts: ShareOptions = { chooserTitle: 'Share files' };
514
- await shareFiles([file], opts);
515
- expect(passedOptions).toEqual(opts);
516
- });
517
-
518
- it('resolves false for an empty array without reaching the backend', async () => {
519
- const backend = fakeBackend();
520
- setShareBackend(backend);
521
- expect(await shareFiles([])).toBe(false);
522
- expect(backend.shared).toBeNull();
523
- });
524
-
525
- it('carries several files in order', async () => {
526
- const backend = fakeBackend();
527
- setShareBackend(backend);
528
- const second: ShareFile = { dataUrl: 'data:text/plain,two', mimeType: 'text/plain', name: 'two.txt' };
529
- await shareFiles([file, second]);
530
- expect(backend.shared?.files).toEqual([file, second]);
171
+ it('dispatches a non-empty portable file tuple', async () => {
172
+ const invoke = vi.fn(async (_content: Parameters<ShareFilesBackend['shareContent']>[0]) => true);
173
+ const host = filesHost({ shareContent: invoke });
174
+ expect(await shareFiles(host, [file])).toBe(true);
175
+ expect(await shareFiles(host, [])).toBe(false);
176
+ expect(invoke).toHaveBeenCalledTimes(1);
177
+ expect(invoke.mock.calls[0]?.[0].files).toEqual([file]);
531
178
  });
532
179
  });
533
180
 
534
181
  describe('shareText', () => {
535
- it('shares via shareContent with text payload', async () => {
536
- const backend = fakeBackend();
537
- setShareBackend(backend);
538
- expect(await shareText('hello world')).toBe(true);
539
- expect(backend.shared).toEqual({ text: 'hello world' });
540
- });
541
-
542
- it('passes options to shareContent', async () => {
543
- let passedOptions: ShareOptions | undefined;
544
- setShareBackend({
545
- isAvailable: () => true,
546
- canShare: () => true,
547
- share: async (_content, options) => {
548
- passedOptions = options;
549
- return true;
550
- },
551
- shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
552
- });
553
- const opts: ShareOptions = { chooserTitle: 'Share text' };
554
- await shareText('hi', opts);
555
- expect(passedOptions).toEqual(opts);
182
+ it('delegates text through the content slot', async () => {
183
+ const invoke = vi.fn(async (_content: Parameters<ShareContentBackend['shareContent']>[0]) => true);
184
+ expect(await shareText(contentHost({ shareContent: invoke }), 'hello')).toBe(true);
185
+ expect(invoke).toHaveBeenCalledWith({ text: 'hello' });
556
186
  });
557
187
  });
558
188
 
559
189
  describe('shareUrl', () => {
560
- it('shares via shareContent with url payload', async () => {
561
- const backend = fakeBackend();
562
- setShareBackend(backend);
563
- expect(await shareUrl('https://example.com')).toBe(true);
564
- expect(backend.shared).toEqual({ url: 'https://example.com' });
565
- });
566
-
567
- it('passes options to shareContent', async () => {
568
- let passedOptions: ShareOptions | undefined;
569
- setShareBackend({
570
- isAvailable: () => true,
571
- canShare: () => true,
572
- share: async (_content, options) => {
573
- passedOptions = options;
574
- return true;
575
- },
576
- shareWithResult: async () => ({ completed: true, activityType: null, dismissed: false }),
577
- });
578
- const opts: ShareOptions = { chooserTitle: 'Share link' };
579
- await shareUrl('https://example.com', opts);
580
- expect(passedOptions).toEqual(opts);
581
- });
582
- });
583
-
584
- describe('web backend file conversion', () => {
585
- it('reports a malformed data URL as unshareable rather than sending the wrong bytes', () => {
586
- // A dataUrl with no comma used to slip through: indexOf returned -1, so the header read as ''
587
- // and the body as the whole string, and the converter produced a plausible File containing the
588
- // URL text. Failing to the ordinary false sentinel is the honest answer.
589
- mockNavigator({ share: () => Promise.resolve(), canShare: () => true });
590
- const backend = createWebShareBackend();
591
- expect(
592
- backend.canShare({ files: [{ dataUrl: 'data:text/plain;base64', mimeType: 'text/plain', name: 'a.txt' }] }),
593
- ).toBe(false);
594
- });
595
-
596
- it('still converts a well-formed data URL', () => {
597
- mockNavigator({ share: () => Promise.resolve(), canShare: () => true });
598
- const backend = createWebShareBackend();
599
- expect(
600
- backend.canShare({ files: [{ dataUrl: 'data:text/plain;base64,aGk=', mimeType: 'text/plain', name: 'a.txt' }] }),
601
- ).toBe(true);
602
- });
603
-
604
- it('rejects a malformed data URL on the share path too, not only the probe', async () => {
605
- mockNavigator({ share: () => Promise.resolve(), canShare: () => true });
606
- const backend = createWebShareBackend();
607
- expect(await backend.share({ files: [{ dataUrl: 'nope', mimeType: 'text/plain', name: 'a.txt' }] })).toBe(false);
190
+ it('delegates a URL through the content slot', async () => {
191
+ const invoke = vi.fn(async (_content: Parameters<ShareContentBackend['shareContent']>[0]) => true);
192
+ expect(await shareUrl(contentHost({ shareContent: invoke }), 'https://flight.dev')).toBe(true);
193
+ expect(invoke).toHaveBeenCalledWith({ url: 'https://flight.dev' });
608
194
  });
609
195
  });
@@ -0,0 +1,30 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
2
+ import type { EntityRuntimeKey, HasShareContent, ShareContentBackend } from '@flighthq/types/contract';
3
+
4
+ import { shareText } from './share';
5
+
6
+ function createRecordingHost(label: string, calls: string[]): HasShareContent {
7
+ const content = createEntity({
8
+ canShareContent: () => true,
9
+ async shareContent(payload) {
10
+ calls.push(`${label}:${payload.text ?? ''}`);
11
+ return true;
12
+ },
13
+ async shareContentWithResult() {
14
+ return { activityType: null, completed: true, dismissed: false };
15
+ },
16
+ } satisfies Omit<ShareContentBackend, typeof EntityRuntimeKey>);
17
+ return { share: { content } };
18
+ }
19
+
20
+ describe('explicit Share host isolation', () => {
21
+ it('routes two live hosts to their own content providers', async () => {
22
+ const calls: string[] = [];
23
+ const first = createRecordingHost('first', calls);
24
+ const second = createRecordingHost('second', calls);
25
+
26
+ expect(await shareText(first, 'alpha')).toBe(true);
27
+ expect(await shareText(second, 'beta')).toBe(true);
28
+ expect(calls).toEqual(['first:alpha', 'second:beta']);
29
+ });
30
+ });