@egen-civitas/esm-platform 0.0.0-stage → 1.2.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +117 -2
  3. package/dist/adapters/web/camera.d.ts +10 -0
  4. package/dist/adapters/web/camera.js +31 -0
  5. package/dist/adapters/web/file-registry.d.ts +7 -0
  6. package/dist/adapters/web/file-registry.js +26 -0
  7. package/dist/adapters/web/files.d.ts +7 -0
  8. package/dist/adapters/web/files.js +22 -0
  9. package/dist/adapters/web/index.d.ts +9 -0
  10. package/dist/adapters/web/index.js +9 -0
  11. package/dist/adapters/web/lifecycle.d.ts +3 -0
  12. package/dist/adapters/web/lifecycle.js +20 -0
  13. package/dist/adapters/web/open-external.d.ts +8 -0
  14. package/dist/adapters/web/open-external.js +32 -0
  15. package/dist/adapters/web/permissions.d.ts +12 -0
  16. package/dist/adapters/web/permissions.js +48 -0
  17. package/dist/adapters/web/pick-files.d.ts +17 -0
  18. package/dist/adapters/web/pick-files.js +49 -0
  19. package/dist/adapters/web/session.d.ts +12 -0
  20. package/dist/adapters/web/session.js +19 -0
  21. package/dist/adapters/web/web-adapter.test.js +1233 -0
  22. package/dist/bridge/client.d.ts +48 -0
  23. package/dist/bridge/client.js +212 -0
  24. package/dist/bridge/client.test.js +1264 -0
  25. package/dist/bridge/index.d.ts +4 -0
  26. package/dist/bridge/index.js +2 -0
  27. package/dist/bridge/protocol.d.ts +72 -0
  28. package/dist/bridge/protocol.js +66 -0
  29. package/dist/bridge/protocol.test.js +215 -0
  30. package/dist/capabilities.d.ts +34 -0
  31. package/dist/capabilities.js +48 -0
  32. package/dist/detect.d.ts +25 -0
  33. package/dist/detect.js +38 -0
  34. package/dist/detect.test.js +23 -0
  35. package/dist/errors.d.ts +27 -0
  36. package/dist/errors.js +27 -0
  37. package/dist/errors.test.js +31 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +14 -0
  40. package/dist/native-platform.test.js +329 -0
  41. package/dist/native.d.ts +24 -0
  42. package/dist/native.js +46 -0
  43. package/dist/platform.d.ts +24 -0
  44. package/dist/platform.js +56 -0
  45. package/dist/platform.test.js +696 -0
  46. package/dist/register-capabilities.test.js +188 -0
  47. package/dist/registry.d.ts +52 -0
  48. package/dist/registry.js +101 -0
  49. package/dist/registry.test.js +681 -0
  50. package/dist/testing/index.d.ts +23 -0
  51. package/dist/testing/index.js +14 -0
  52. package/dist/testing/mock-host.d.ts +49 -0
  53. package/dist/testing/mock-host.js +86 -0
  54. package/dist/types.d.ts +108 -0
  55. package/dist/types.js +1 -0
  56. package/package.json +57 -3
@@ -0,0 +1,1233 @@
1
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
2
+ try {
3
+ var info = gen[key](arg);
4
+ var value = info.value;
5
+ } catch (error) {
6
+ reject(error);
7
+ return;
8
+ }
9
+ if (info.done) {
10
+ resolve(value);
11
+ } else {
12
+ Promise.resolve(value).then(_next, _throw);
13
+ }
14
+ }
15
+ function _async_to_generator(fn) {
16
+ return function() {
17
+ var self = this, args = arguments;
18
+ return new Promise(function(resolve, reject) {
19
+ var gen = fn.apply(self, args);
20
+ function _next(value) {
21
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
22
+ }
23
+ function _throw(err) {
24
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
25
+ }
26
+ _next(undefined);
27
+ });
28
+ };
29
+ }
30
+ function _ts_generator(thisArg, body) {
31
+ var f, y, t, _ = {
32
+ label: 0,
33
+ sent: function() {
34
+ if (t[0] & 1) throw t[1];
35
+ return t[1];
36
+ },
37
+ trys: [],
38
+ ops: []
39
+ }, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype), d = Object.defineProperty;
40
+ return d(g, "next", {
41
+ value: verb(0)
42
+ }), d(g, "throw", {
43
+ value: verb(1)
44
+ }), d(g, "return", {
45
+ value: verb(2)
46
+ }), typeof Symbol === "function" && d(g, Symbol.iterator, {
47
+ value: function() {
48
+ return this;
49
+ }
50
+ }), g;
51
+ function verb(n) {
52
+ return function(v) {
53
+ return step([
54
+ n,
55
+ v
56
+ ]);
57
+ };
58
+ }
59
+ function step(op) {
60
+ if (f) throw new TypeError("Generator is already executing.");
61
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
62
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
63
+ if (y = 0, t) op = [
64
+ op[0] & 2,
65
+ t.value
66
+ ];
67
+ switch(op[0]){
68
+ case 0:
69
+ case 1:
70
+ t = op;
71
+ break;
72
+ case 4:
73
+ _.label++;
74
+ return {
75
+ value: op[1],
76
+ done: false
77
+ };
78
+ case 5:
79
+ _.label++;
80
+ y = op[1];
81
+ op = [
82
+ 0
83
+ ];
84
+ continue;
85
+ case 7:
86
+ op = _.ops.pop();
87
+ _.trys.pop();
88
+ continue;
89
+ default:
90
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
91
+ _ = 0;
92
+ continue;
93
+ }
94
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
95
+ _.label = op[1];
96
+ break;
97
+ }
98
+ if (op[0] === 6 && _.label < t[1]) {
99
+ _.label = t[1];
100
+ t = op;
101
+ break;
102
+ }
103
+ if (t && _.label < t[2]) {
104
+ _.label = t[2];
105
+ _.ops.push(op);
106
+ break;
107
+ }
108
+ if (t[2]) _.ops.pop();
109
+ _.trys.pop();
110
+ continue;
111
+ }
112
+ op = body.call(thisArg, _);
113
+ } catch (e) {
114
+ op = [
115
+ 6,
116
+ e
117
+ ];
118
+ y = 0;
119
+ } finally{
120
+ f = t = 0;
121
+ }
122
+ if (op[0] & 5) throw op[1];
123
+ return {
124
+ value: op[0] ? op[1] : void 0,
125
+ done: true
126
+ };
127
+ }
128
+ }
129
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
130
+ import { getPlatform } from '../../platform';
131
+ import { isPlatformError } from '../../errors';
132
+ import { createWebCapabilities } from './index';
133
+ import { getWebFile, releaseWebFile } from './file-registry';
134
+ import { assertExternalUrl } from './open-external';
135
+ var w = window;
136
+ /** Simule le sélecteur de fichiers : à `click()`, l'input reçoit `files` (ou un événement `cancel`). */ function stubFilePicker(behaviour) {
137
+ var inputs = [];
138
+ vi.spyOn(HTMLInputElement.prototype, 'click').mockImplementation(function() {
139
+ var _this = this;
140
+ inputs.push(this);
141
+ // Le navigateur émet l'événement après le retour de `click()`.
142
+ queueMicrotask(function() {
143
+ var _behaviour_files;
144
+ if (behaviour.cancel) {
145
+ _this.dispatchEvent(new Event('cancel'));
146
+ return;
147
+ }
148
+ Object.defineProperty(_this, 'files', {
149
+ value: (_behaviour_files = behaviour.files) !== null && _behaviour_files !== void 0 ? _behaviour_files : [],
150
+ configurable: true
151
+ });
152
+ _this.dispatchEvent(new Event('change'));
153
+ });
154
+ });
155
+ return inputs;
156
+ }
157
+ /** happy-dom fournit une API Permissions : l'absence doit être simulée explicitement. */ var removePermissionsApi = function removePermissionsApi() {
158
+ return Object.defineProperty(navigator, 'permissions', {
159
+ value: undefined,
160
+ configurable: true
161
+ });
162
+ };
163
+ var pdf = function pdf() {
164
+ return new File([
165
+ 'contenu'
166
+ ], 'bilan.pdf', {
167
+ type: 'application/pdf'
168
+ });
169
+ };
170
+ var rejection = function rejection(promise) {
171
+ return promise.then(function() {
172
+ throw new Error('devait rejeter');
173
+ }, function(error) {
174
+ return error;
175
+ });
176
+ };
177
+ beforeEach(function() {
178
+ delete w.egenTarget;
179
+ });
180
+ afterEach(function() {
181
+ vi.restoreAllMocks();
182
+ delete navigator.permissions;
183
+ delete w.Notification;
184
+ document.body.innerHTML = '';
185
+ });
186
+ describe('session (fournisseur vide : le cookie reste le mécanisme)', function() {
187
+ var session = getPlatform().session;
188
+ it('getAuthorization → null, refresh → false, clear → no-op', function() {
189
+ return _async_to_generator(function() {
190
+ return _ts_generator(this, function(_state) {
191
+ switch(_state.label){
192
+ case 0:
193
+ return [
194
+ 4,
195
+ expect(session.getAuthorization()).resolves.toBeNull()
196
+ ];
197
+ case 1:
198
+ _state.sent();
199
+ return [
200
+ 4,
201
+ expect(session.refresh()).resolves.toBe(false)
202
+ ];
203
+ case 2:
204
+ _state.sent();
205
+ return [
206
+ 4,
207
+ expect(session.clear()).resolves.toBeUndefined()
208
+ ];
209
+ case 3:
210
+ _state.sent();
211
+ return [
212
+ 2
213
+ ];
214
+ }
215
+ });
216
+ })();
217
+ });
218
+ it('setTokens → UNAVAILABLE : aucun jeton n’est stocké dans le navigateur', function() {
219
+ return _async_to_generator(function() {
220
+ var error;
221
+ return _ts_generator(this, function(_state) {
222
+ switch(_state.label){
223
+ case 0:
224
+ return [
225
+ 4,
226
+ rejection(session.setTokens({
227
+ access: 'SECRET-ACCESS'
228
+ }))
229
+ ];
230
+ case 1:
231
+ error = _state.sent();
232
+ expect(error).toMatchObject({
233
+ code: 'UNAVAILABLE',
234
+ capability: 'session',
235
+ method: 'setTokens'
236
+ });
237
+ expect(String(error.message)).not.toContain('SECRET-ACCESS');
238
+ expect(JSON.stringify(localStorage)).not.toContain('SECRET-ACCESS');
239
+ expect(JSON.stringify(sessionStorage)).not.toContain('SECRET-ACCESS');
240
+ return [
241
+ 2
242
+ ];
243
+ }
244
+ });
245
+ })();
246
+ });
247
+ });
248
+ describe('secureStorage, push, biometric : jamais simulés', function() {
249
+ it.each([
250
+ [
251
+ 'secureStorage',
252
+ 'get'
253
+ ],
254
+ [
255
+ 'secureStorage',
256
+ 'set'
257
+ ],
258
+ [
259
+ 'secureStorage',
260
+ 'remove'
261
+ ],
262
+ [
263
+ 'push',
264
+ 'register'
265
+ ],
266
+ [
267
+ 'biometric',
268
+ 'isAvailable'
269
+ ],
270
+ [
271
+ 'biometric',
272
+ 'authenticate'
273
+ ]
274
+ ])('%s.%s → UNAVAILABLE en Web', function(capability, method) {
275
+ return _async_to_generator(function() {
276
+ var platform, target;
277
+ return _ts_generator(this, function(_state) {
278
+ switch(_state.label){
279
+ case 0:
280
+ platform = getPlatform();
281
+ expect(platform.has(capability)).toBe(false);
282
+ target = platform[capability];
283
+ return [
284
+ 4,
285
+ expect(target[method]('k', 'v')).rejects.toMatchObject({
286
+ code: 'UNAVAILABLE',
287
+ capability: capability,
288
+ method: method
289
+ })
290
+ ];
291
+ case 1:
292
+ _state.sent();
293
+ return [
294
+ 2
295
+ ];
296
+ }
297
+ });
298
+ })();
299
+ });
300
+ it('secureStorage.set n’écrit rien dans localStorage (pas de faux coffre)', function() {
301
+ return _async_to_generator(function() {
302
+ return _ts_generator(this, function(_state) {
303
+ switch(_state.label){
304
+ case 0:
305
+ return [
306
+ 4,
307
+ getPlatform().secureStorage.set('clé', 'valeur-secrète').catch(function() {})
308
+ ];
309
+ case 1:
310
+ _state.sent();
311
+ expect(localStorage.getItem('clé')).toBeNull();
312
+ expect(JSON.stringify(localStorage)).not.toContain('valeur-secrète');
313
+ return [
314
+ 2
315
+ ];
316
+ }
317
+ });
318
+ })();
319
+ });
320
+ });
321
+ describe('openExternal', function() {
322
+ it.each([
323
+ 'https://exemple.ga/page?x=1',
324
+ 'mailto:contact@exemple.ga',
325
+ 'tel:+24101020304'
326
+ ])('ouvre %s avec window.open(_blank, noopener,noreferrer)', function(url) {
327
+ return _async_to_generator(function() {
328
+ var open;
329
+ return _ts_generator(this, function(_state) {
330
+ switch(_state.label){
331
+ case 0:
332
+ open = vi.spyOn(window, 'open').mockReturnValue(null);
333
+ return [
334
+ 4,
335
+ getPlatform().openExternal.open(url)
336
+ ];
337
+ case 1:
338
+ _state.sent();
339
+ expect(open).toHaveBeenCalledTimes(1);
340
+ expect(open).toHaveBeenCalledWith(new URL(url).href, '_blank', 'noopener,noreferrer');
341
+ return [
342
+ 2
343
+ ];
344
+ }
345
+ });
346
+ })();
347
+ });
348
+ it.each([
349
+ 'http://exemple.ga',
350
+ 'javascript:alert(1)',
351
+ 'data:text/html,<script>alert(1)</script>',
352
+ 'file:///etc/passwd',
353
+ 'ftp://exemple.ga',
354
+ '//exemple.ga',
355
+ '/chemin/relatif',
356
+ 'pas une url',
357
+ ''
358
+ ])('refuse %j (INVALID_ARGS) sans rien ouvrir', function(url) {
359
+ return _async_to_generator(function() {
360
+ var open, error;
361
+ return _ts_generator(this, function(_state) {
362
+ switch(_state.label){
363
+ case 0:
364
+ open = vi.spyOn(window, 'open').mockReturnValue(null);
365
+ return [
366
+ 4,
367
+ rejection(getPlatform().openExternal.open(url))
368
+ ];
369
+ case 1:
370
+ error = _state.sent();
371
+ expect(error).toMatchObject({
372
+ code: 'INVALID_ARGS',
373
+ capability: 'openExternal',
374
+ method: 'open'
375
+ });
376
+ expect(open).not.toHaveBeenCalled();
377
+ return [
378
+ 2
379
+ ];
380
+ }
381
+ });
382
+ })();
383
+ });
384
+ it('refuse une valeur qui n’est pas une chaîne', function() {
385
+ return _async_to_generator(function() {
386
+ return _ts_generator(this, function(_state) {
387
+ switch(_state.label){
388
+ case 0:
389
+ return [
390
+ 4,
391
+ expect(getPlatform().openExternal.open(undefined)).rejects.toMatchObject({
392
+ code: 'INVALID_ARGS'
393
+ })
394
+ ];
395
+ case 1:
396
+ _state.sent();
397
+ return [
398
+ 2
399
+ ];
400
+ }
401
+ });
402
+ })();
403
+ });
404
+ it('le message d’erreur ne reprend jamais l’URL (elle peut contenir un secret)', function() {
405
+ try {
406
+ assertExternalUrl('javascript:steal("TOKEN-SECRET")');
407
+ } catch (error) {
408
+ expect(error.message).not.toContain('TOKEN-SECRET');
409
+ }
410
+ });
411
+ });
412
+ describe('lifecycle (visibilitychange)', function() {
413
+ var setVisibility = function setVisibility(state) {
414
+ Object.defineProperty(document, 'visibilityState', {
415
+ configurable: true,
416
+ get: function get() {
417
+ return state;
418
+ }
419
+ });
420
+ document.dispatchEvent(new Event('visibilitychange'));
421
+ };
422
+ afterEach(function() {
423
+ delete document.visibilityState;
424
+ });
425
+ it('onResume ne se déclenche que quand la page redevient visible ; onPause quand elle est masquée', function() {
426
+ var lifecycle = getPlatform().lifecycle;
427
+ var resume = vi.fn();
428
+ var pause = vi.fn();
429
+ var offResume = lifecycle.onResume(resume);
430
+ var offPause = lifecycle.onPause(pause);
431
+ setVisibility('hidden');
432
+ expect(pause).toHaveBeenCalledTimes(1);
433
+ expect(resume).not.toHaveBeenCalled();
434
+ setVisibility('visible');
435
+ expect(resume).toHaveBeenCalledTimes(1);
436
+ expect(pause).toHaveBeenCalledTimes(1);
437
+ offResume();
438
+ offPause();
439
+ });
440
+ it('la fonction retournée désabonne', function() {
441
+ var resume = vi.fn();
442
+ var off = getPlatform().lifecycle.onResume(resume);
443
+ off();
444
+ setVisibility('visible');
445
+ expect(resume).not.toHaveBeenCalled();
446
+ });
447
+ it('plusieurs abonnés sont indépendants', function() {
448
+ var lifecycle = getPlatform().lifecycle;
449
+ var a = vi.fn();
450
+ var b = vi.fn();
451
+ var offA = lifecycle.onResume(a);
452
+ var offB = lifecycle.onResume(b);
453
+ offA();
454
+ setVisibility('visible');
455
+ expect(a).not.toHaveBeenCalled();
456
+ expect(b).toHaveBeenCalledTimes(1);
457
+ offB();
458
+ });
459
+ it('un callback qui n’est pas une fonction lève INVALID_ARGS', function() {
460
+ expect(function() {
461
+ return getPlatform().lifecycle.onResume('x');
462
+ }).toThrowError(expect.objectContaining({
463
+ code: 'INVALID_ARGS',
464
+ method: 'onResume'
465
+ }));
466
+ });
467
+ });
468
+ describe('files', function() {
469
+ it('select : <input type="file"> éphémère, handles sans binaire, input retiré du document', function() {
470
+ return _async_to_generator(function() {
471
+ var inputs, handles;
472
+ return _ts_generator(this, function(_state) {
473
+ switch(_state.label){
474
+ case 0:
475
+ inputs = stubFilePicker({
476
+ files: [
477
+ pdf()
478
+ ]
479
+ });
480
+ return [
481
+ 4,
482
+ getPlatform().files.select({
483
+ accept: [
484
+ 'application/pdf',
485
+ '.csv'
486
+ ]
487
+ })
488
+ ];
489
+ case 1:
490
+ handles = _state.sent();
491
+ expect(inputs).toHaveLength(1);
492
+ expect(inputs[0].type).toBe('file');
493
+ expect(inputs[0].accept).toBe('application/pdf,.csv');
494
+ expect(inputs[0].multiple).toBe(false);
495
+ expect(document.body.contains(inputs[0])).toBe(false);
496
+ expect(handles).toEqual([
497
+ {
498
+ id: expect.any(String),
499
+ name: 'bilan.pdf',
500
+ size: 7,
501
+ mime: 'application/pdf'
502
+ }
503
+ ]);
504
+ expect(Object.keys(handles[0]).sort()).toEqual([
505
+ 'id',
506
+ 'mime',
507
+ 'name',
508
+ 'size'
509
+ ]);
510
+ return [
511
+ 2
512
+ ];
513
+ }
514
+ });
515
+ })();
516
+ });
517
+ it('select multiple : un handle par fichier, identifiants distincts, fichiers retrouvables puis libérables', function() {
518
+ return _async_to_generator(function() {
519
+ var _getWebFile, inputs, handles;
520
+ return _ts_generator(this, function(_state) {
521
+ switch(_state.label){
522
+ case 0:
523
+ inputs = stubFilePicker({
524
+ files: [
525
+ pdf(),
526
+ new File([
527
+ 'x'
528
+ ], 'a.png', {
529
+ type: ''
530
+ })
531
+ ]
532
+ });
533
+ return [
534
+ 4,
535
+ getPlatform().files.select({
536
+ multiple: true
537
+ })
538
+ ];
539
+ case 1:
540
+ handles = _state.sent();
541
+ expect(inputs[0].multiple).toBe(true);
542
+ expect(new Set(handles.map(function(h) {
543
+ return h.id;
544
+ })).size).toBe(2);
545
+ expect(handles[1].mime).toBe('application/octet-stream');
546
+ expect((_getWebFile = getWebFile(handles[0].id)) === null || _getWebFile === void 0 ? void 0 : _getWebFile.name).toBe('bilan.pdf');
547
+ expect(releaseWebFile(handles[0].id)).toBe(true);
548
+ expect(getWebFile(handles[0].id)).toBeUndefined();
549
+ expect(releaseWebFile(handles[0].id)).toBe(false);
550
+ return [
551
+ 2
552
+ ];
553
+ }
554
+ });
555
+ })();
556
+ });
557
+ it('annulation (événement cancel) → CANCELLED, input retiré', function() {
558
+ return _async_to_generator(function() {
559
+ var inputs, error;
560
+ return _ts_generator(this, function(_state) {
561
+ switch(_state.label){
562
+ case 0:
563
+ inputs = stubFilePicker({
564
+ cancel: true
565
+ });
566
+ return [
567
+ 4,
568
+ rejection(getPlatform().files.select())
569
+ ];
570
+ case 1:
571
+ error = _state.sent();
572
+ expect(error).toMatchObject({
573
+ code: 'CANCELLED',
574
+ capability: 'files',
575
+ method: 'select'
576
+ });
577
+ expect(document.body.contains(inputs[0])).toBe(false);
578
+ return [
579
+ 2
580
+ ];
581
+ }
582
+ });
583
+ })();
584
+ });
585
+ it('aucun fichier choisi → CANCELLED', function() {
586
+ return _async_to_generator(function() {
587
+ return _ts_generator(this, function(_state) {
588
+ switch(_state.label){
589
+ case 0:
590
+ stubFilePicker({
591
+ files: []
592
+ });
593
+ return [
594
+ 4,
595
+ expect(getPlatform().files.select()).rejects.toMatchObject({
596
+ code: 'CANCELLED'
597
+ })
598
+ ];
599
+ case 1:
600
+ _state.sent();
601
+ return [
602
+ 2
603
+ ];
604
+ }
605
+ });
606
+ })();
607
+ });
608
+ it.each([
609
+ [
610
+ '<script>'
611
+ ],
612
+ [
613
+ 'image/*;x'
614
+ ],
615
+ [
616
+ ''
617
+ ],
618
+ [
619
+ 'a b'
620
+ ]
621
+ ])('accept invalide %j → INVALID_ARGS, sélecteur jamais ouvert', function(entry) {
622
+ return _async_to_generator(function() {
623
+ var inputs;
624
+ return _ts_generator(this, function(_state) {
625
+ switch(_state.label){
626
+ case 0:
627
+ inputs = stubFilePicker({
628
+ files: [
629
+ pdf()
630
+ ]
631
+ });
632
+ return [
633
+ 4,
634
+ expect(getPlatform().files.select({
635
+ accept: [
636
+ entry
637
+ ]
638
+ })).rejects.toMatchObject({
639
+ code: 'INVALID_ARGS'
640
+ })
641
+ ];
642
+ case 1:
643
+ _state.sent();
644
+ expect(inputs).toHaveLength(0);
645
+ return [
646
+ 2
647
+ ];
648
+ }
649
+ });
650
+ })();
651
+ });
652
+ it('download n’est pas fourni en Web (UNAVAILABLE), select l’est', function() {
653
+ return _async_to_generator(function() {
654
+ var platform;
655
+ return _ts_generator(this, function(_state) {
656
+ switch(_state.label){
657
+ case 0:
658
+ platform = getPlatform();
659
+ expect(platform.has('files')).toBe(true);
660
+ return [
661
+ 4,
662
+ expect(platform.files.download('https://exemple.ga/f.pdf', 'f.pdf')).rejects.toMatchObject({
663
+ code: 'UNAVAILABLE',
664
+ method: 'download'
665
+ })
666
+ ];
667
+ case 1:
668
+ _state.sent();
669
+ return [
670
+ 2
671
+ ];
672
+ }
673
+ });
674
+ })();
675
+ });
676
+ });
677
+ describe('camera (input capture — choix documenté dans camera.ts)', function() {
678
+ it('capture arrière par défaut : accept image/*, attribut capture=environment', function() {
679
+ return _async_to_generator(function() {
680
+ var inputs, handle, capture;
681
+ return _ts_generator(this, function(_state) {
682
+ switch(_state.label){
683
+ case 0:
684
+ inputs = stubFilePicker({
685
+ files: [
686
+ new File([
687
+ 'p'
688
+ ], 'photo.jpg', {
689
+ type: 'image/jpeg'
690
+ })
691
+ ]
692
+ });
693
+ return [
694
+ 4,
695
+ getPlatform().camera.capture()
696
+ ];
697
+ case 1:
698
+ handle = _state.sent();
699
+ expect(inputs[0].accept).toBe('image/*');
700
+ capture = inputs[0].getAttribute('capture');
701
+ expect(capture).toBe('environment');
702
+ expect(handle).toMatchObject({
703
+ name: 'photo.jpg',
704
+ mime: 'image/jpeg',
705
+ size: 1
706
+ });
707
+ return [
708
+ 2
709
+ ];
710
+ }
711
+ });
712
+ })();
713
+ });
714
+ it('facing "front" → capture=user ; "back" → environment', function() {
715
+ return _async_to_generator(function() {
716
+ var file, front, frontCapture, back, backCapture;
717
+ return _ts_generator(this, function(_state) {
718
+ switch(_state.label){
719
+ case 0:
720
+ file = new File([
721
+ 'p'
722
+ ], 'photo.jpg', {
723
+ type: 'image/jpeg'
724
+ });
725
+ front = stubFilePicker({
726
+ files: [
727
+ file
728
+ ]
729
+ });
730
+ return [
731
+ 4,
732
+ getPlatform().camera.capture({
733
+ facing: 'front'
734
+ })
735
+ ];
736
+ case 1:
737
+ _state.sent();
738
+ frontCapture = front[0].getAttribute('capture');
739
+ expect(frontCapture).toBe('user');
740
+ vi.restoreAllMocks();
741
+ back = stubFilePicker({
742
+ files: [
743
+ file
744
+ ]
745
+ });
746
+ return [
747
+ 4,
748
+ getPlatform().camera.capture({
749
+ facing: 'back'
750
+ })
751
+ ];
752
+ case 2:
753
+ _state.sent();
754
+ backCapture = back[0].getAttribute('capture');
755
+ expect(backCapture).toBe('environment');
756
+ return [
757
+ 2
758
+ ];
759
+ }
760
+ });
761
+ })();
762
+ });
763
+ it('facing invalide → INVALID_ARGS ; annulation → CANCELLED', function() {
764
+ return _async_to_generator(function() {
765
+ return _ts_generator(this, function(_state) {
766
+ switch(_state.label){
767
+ case 0:
768
+ return [
769
+ 4,
770
+ expect(getPlatform().camera.capture({
771
+ facing: 'left'
772
+ })).rejects.toMatchObject({
773
+ code: 'INVALID_ARGS'
774
+ })
775
+ ];
776
+ case 1:
777
+ _state.sent();
778
+ stubFilePicker({
779
+ cancel: true
780
+ });
781
+ return [
782
+ 4,
783
+ expect(getPlatform().camera.capture()).rejects.toMatchObject({
784
+ code: 'CANCELLED',
785
+ capability: 'camera'
786
+ })
787
+ ];
788
+ case 2:
789
+ _state.sent();
790
+ return [
791
+ 2
792
+ ];
793
+ }
794
+ });
795
+ })();
796
+ });
797
+ });
798
+ describe('permissions (API Permissions du navigateur)', function() {
799
+ var stubPermissions = function stubPermissions(query) {
800
+ return Object.defineProperty(navigator, 'permissions', {
801
+ value: {
802
+ query: query
803
+ },
804
+ configurable: true
805
+ });
806
+ };
807
+ it('sans API Permissions : capacité non fournie (has faux), méthodes UNAVAILABLE', function() {
808
+ return _async_to_generator(function() {
809
+ var platform;
810
+ return _ts_generator(this, function(_state) {
811
+ switch(_state.label){
812
+ case 0:
813
+ removePermissionsApi();
814
+ platform = getPlatform();
815
+ expect(platform.has('permissions')).toBe(false);
816
+ return [
817
+ 4,
818
+ expect(platform.permissions.check('camera')).rejects.toMatchObject({
819
+ code: 'UNAVAILABLE'
820
+ })
821
+ ];
822
+ case 1:
823
+ _state.sent();
824
+ return [
825
+ 2
826
+ ];
827
+ }
828
+ });
829
+ })();
830
+ });
831
+ it.each([
832
+ 'granted',
833
+ 'denied',
834
+ 'prompt'
835
+ ])('check retourne l’état du navigateur (%s)', function(state) {
836
+ return _async_to_generator(function() {
837
+ var query, platform;
838
+ return _ts_generator(this, function(_state) {
839
+ switch(_state.label){
840
+ case 0:
841
+ query = vi.fn(function() {
842
+ return _async_to_generator(function() {
843
+ return _ts_generator(this, function(_state) {
844
+ return [
845
+ 2,
846
+ {
847
+ state: state
848
+ }
849
+ ];
850
+ });
851
+ })();
852
+ });
853
+ stubPermissions(query);
854
+ platform = getPlatform();
855
+ expect(platform.has('permissions')).toBe(true);
856
+ return [
857
+ 4,
858
+ expect(platform.permissions.check('microphone')).resolves.toBe(state)
859
+ ];
860
+ case 1:
861
+ _state.sent();
862
+ expect(query).toHaveBeenCalledWith({
863
+ name: 'microphone'
864
+ });
865
+ return [
866
+ 2
867
+ ];
868
+ }
869
+ });
870
+ })();
871
+ });
872
+ it('un nom que le navigateur ne connaît pas → UNAVAILABLE (jamais une valeur inventée)', function() {
873
+ return _async_to_generator(function() {
874
+ return _ts_generator(this, function(_state) {
875
+ switch(_state.label){
876
+ case 0:
877
+ stubPermissions(function() {
878
+ return _async_to_generator(function() {
879
+ return _ts_generator(this, function(_state) {
880
+ throw new TypeError('inconnu');
881
+ });
882
+ })();
883
+ });
884
+ return [
885
+ 4,
886
+ expect(getPlatform().permissions.check('camera')).rejects.toMatchObject({
887
+ code: 'UNAVAILABLE',
888
+ method: 'check'
889
+ })
890
+ ];
891
+ case 1:
892
+ _state.sent();
893
+ return [
894
+ 2
895
+ ];
896
+ }
897
+ });
898
+ })();
899
+ });
900
+ it('files n’a pas d’équivalent Web → UNAVAILABLE, sans interroger le navigateur', function() {
901
+ return _async_to_generator(function() {
902
+ var query;
903
+ return _ts_generator(this, function(_state) {
904
+ switch(_state.label){
905
+ case 0:
906
+ query = vi.fn(function() {
907
+ return _async_to_generator(function() {
908
+ return _ts_generator(this, function(_state) {
909
+ return [
910
+ 2,
911
+ {
912
+ state: 'granted'
913
+ }
914
+ ];
915
+ });
916
+ })();
917
+ });
918
+ stubPermissions(query);
919
+ return [
920
+ 4,
921
+ expect(getPlatform().permissions.check('files')).rejects.toMatchObject({
922
+ code: 'UNAVAILABLE'
923
+ })
924
+ ];
925
+ case 1:
926
+ _state.sent();
927
+ expect(query).not.toHaveBeenCalled();
928
+ return [
929
+ 2
930
+ ];
931
+ }
932
+ });
933
+ })();
934
+ });
935
+ it('nom inconnu → INVALID_ARGS', function() {
936
+ return _async_to_generator(function() {
937
+ return _ts_generator(this, function(_state) {
938
+ switch(_state.label){
939
+ case 0:
940
+ stubPermissions(function() {
941
+ return _async_to_generator(function() {
942
+ return _ts_generator(this, function(_state) {
943
+ return [
944
+ 2,
945
+ {
946
+ state: 'granted'
947
+ }
948
+ ];
949
+ });
950
+ })();
951
+ });
952
+ return [
953
+ 4,
954
+ expect(getPlatform().permissions.check('gps')).rejects.toMatchObject({
955
+ code: 'INVALID_ARGS'
956
+ })
957
+ ];
958
+ case 1:
959
+ _state.sent();
960
+ return [
961
+ 4,
962
+ expect(getPlatform().permissions.request('gps')).rejects.toMatchObject({
963
+ code: 'INVALID_ARGS'
964
+ })
965
+ ];
966
+ case 2:
967
+ _state.sent();
968
+ return [
969
+ 2
970
+ ];
971
+ }
972
+ });
973
+ })();
974
+ });
975
+ it.each([
976
+ [
977
+ 'granted',
978
+ 'granted'
979
+ ],
980
+ [
981
+ 'denied',
982
+ 'denied'
983
+ ],
984
+ [
985
+ 'default',
986
+ 'denied'
987
+ ]
988
+ ])('request(notifications) : Notification.requestPermission = %s → %s', function(browser, expected) {
989
+ return _async_to_generator(function() {
990
+ return _ts_generator(this, function(_state) {
991
+ switch(_state.label){
992
+ case 0:
993
+ stubPermissions(function() {
994
+ return _async_to_generator(function() {
995
+ return _ts_generator(this, function(_state) {
996
+ return [
997
+ 2,
998
+ {
999
+ state: 'prompt'
1000
+ }
1001
+ ];
1002
+ });
1003
+ })();
1004
+ });
1005
+ w.Notification = {
1006
+ requestPermission: vi.fn(function() {
1007
+ return _async_to_generator(function() {
1008
+ return _ts_generator(this, function(_state) {
1009
+ return [
1010
+ 2,
1011
+ browser
1012
+ ];
1013
+ });
1014
+ })();
1015
+ })
1016
+ };
1017
+ return [
1018
+ 4,
1019
+ expect(getPlatform().permissions.request('notifications')).resolves.toBe(expected)
1020
+ ];
1021
+ case 1:
1022
+ _state.sent();
1023
+ return [
1024
+ 2
1025
+ ];
1026
+ }
1027
+ });
1028
+ })();
1029
+ });
1030
+ it.each([
1031
+ 'camera',
1032
+ 'microphone',
1033
+ 'files'
1034
+ ])('request(%s) → UNAVAILABLE (le navigateur demande à l’usage)', function(name) {
1035
+ return _async_to_generator(function() {
1036
+ return _ts_generator(this, function(_state) {
1037
+ switch(_state.label){
1038
+ case 0:
1039
+ stubPermissions(function() {
1040
+ return _async_to_generator(function() {
1041
+ return _ts_generator(this, function(_state) {
1042
+ return [
1043
+ 2,
1044
+ {
1045
+ state: 'prompt'
1046
+ }
1047
+ ];
1048
+ });
1049
+ })();
1050
+ });
1051
+ return [
1052
+ 4,
1053
+ expect(getPlatform().permissions.request(name)).rejects.toMatchObject({
1054
+ code: 'UNAVAILABLE',
1055
+ method: 'request'
1056
+ })
1057
+ ];
1058
+ case 1:
1059
+ _state.sent();
1060
+ return [
1061
+ 2
1062
+ ];
1063
+ }
1064
+ });
1065
+ })();
1066
+ });
1067
+ });
1068
+ describe('assemblage Web', function() {
1069
+ it('createWebCapabilities : exactement les capacités fournies par un navigateur sans API Permissions', function() {
1070
+ removePermissionsApi();
1071
+ expect(Object.keys(createWebCapabilities()).sort()).toEqual([
1072
+ 'camera',
1073
+ 'files',
1074
+ 'lifecycle',
1075
+ 'openExternal',
1076
+ 'session'
1077
+ ]);
1078
+ });
1079
+ it('getPlatform() en Web : has() reflète les capacités réellement fournies', function() {
1080
+ return _async_to_generator(function() {
1081
+ var platform, provided, absent, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, name, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, name1;
1082
+ return _ts_generator(this, function(_state) {
1083
+ switch(_state.label){
1084
+ case 0:
1085
+ Object.defineProperty(navigator, 'permissions', {
1086
+ value: {
1087
+ query: function query() {
1088
+ return _async_to_generator(function() {
1089
+ return _ts_generator(this, function(_state) {
1090
+ return [
1091
+ 2,
1092
+ {
1093
+ state: 'prompt'
1094
+ }
1095
+ ];
1096
+ });
1097
+ })();
1098
+ }
1099
+ },
1100
+ configurable: true
1101
+ });
1102
+ platform = getPlatform();
1103
+ expect(platform.environment).toBe('web');
1104
+ return [
1105
+ 4,
1106
+ expect(platform.ready).resolves.toBeUndefined()
1107
+ ];
1108
+ case 1:
1109
+ _state.sent();
1110
+ provided = [
1111
+ 'session',
1112
+ 'openExternal',
1113
+ 'lifecycle',
1114
+ 'files',
1115
+ 'camera',
1116
+ 'permissions'
1117
+ ];
1118
+ absent = [
1119
+ 'secureStorage',
1120
+ 'push',
1121
+ 'biometric'
1122
+ ];
1123
+ _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1124
+ try {
1125
+ for(_iterator = provided[Symbol.iterator](); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1126
+ name = _step.value;
1127
+ expect(platform.has(name)).toBe(true);
1128
+ }
1129
+ } catch (err) {
1130
+ _didIteratorError = true;
1131
+ _iteratorError = err;
1132
+ } finally{
1133
+ try {
1134
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1135
+ _iterator.return();
1136
+ }
1137
+ } finally{
1138
+ if (_didIteratorError) {
1139
+ throw _iteratorError;
1140
+ }
1141
+ }
1142
+ }
1143
+ _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
1144
+ try {
1145
+ for(_iterator1 = absent[Symbol.iterator](); !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
1146
+ name1 = _step1.value;
1147
+ expect(platform.has(name1)).toBe(false);
1148
+ }
1149
+ } catch (err) {
1150
+ _didIteratorError1 = true;
1151
+ _iteratorError1 = err;
1152
+ } finally{
1153
+ try {
1154
+ if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
1155
+ _iterator1.return();
1156
+ }
1157
+ } finally{
1158
+ if (_didIteratorError1) {
1159
+ throw _iteratorError1;
1160
+ }
1161
+ }
1162
+ }
1163
+ return [
1164
+ 2
1165
+ ];
1166
+ }
1167
+ });
1168
+ })();
1169
+ });
1170
+ it('en natif, l’adaptateur Web n’est JAMAIS utilisé (aucune capacité du navigateur ne fuit vers un Host absent)', function() {
1171
+ w.egenTarget = 'android';
1172
+ var platform = getPlatform();
1173
+ for(var _i = 0, _iter = [
1174
+ 'session',
1175
+ 'openExternal',
1176
+ 'lifecycle',
1177
+ 'files',
1178
+ 'camera',
1179
+ 'permissions',
1180
+ 'secureStorage',
1181
+ 'push',
1182
+ 'biometric'
1183
+ ]; _i < _iter.length; _i++){
1184
+ var name = _iter[_i];
1185
+ expect(platform.has(name)).toBe(false);
1186
+ }
1187
+ });
1188
+ it('toute erreur d’une capacité Web est une PlatformError (jamais une Error générique)', function() {
1189
+ return _async_to_generator(function() {
1190
+ var errors, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, error;
1191
+ return _ts_generator(this, function(_state) {
1192
+ switch(_state.label){
1193
+ case 0:
1194
+ return [
1195
+ 4,
1196
+ Promise.all([
1197
+ rejection(getPlatform().openExternal.open('http://x')),
1198
+ rejection(getPlatform().files.download('https://x', 'f')),
1199
+ rejection(getPlatform().session.setTokens({
1200
+ access: 'a'
1201
+ }))
1202
+ ])
1203
+ ];
1204
+ case 1:
1205
+ errors = _state.sent();
1206
+ _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1207
+ try {
1208
+ for(_iterator = errors[Symbol.iterator](); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1209
+ error = _step.value;
1210
+ expect(isPlatformError(error)).toBe(true);
1211
+ }
1212
+ } catch (err) {
1213
+ _didIteratorError = true;
1214
+ _iteratorError = err;
1215
+ } finally{
1216
+ try {
1217
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1218
+ _iterator.return();
1219
+ }
1220
+ } finally{
1221
+ if (_didIteratorError) {
1222
+ throw _iteratorError;
1223
+ }
1224
+ }
1225
+ }
1226
+ return [
1227
+ 2
1228
+ ];
1229
+ }
1230
+ });
1231
+ })();
1232
+ });
1233
+ });