@sasjs/cli 6.16.0 → 6.18.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.
@@ -31,6 +31,31 @@
31
31
  if (DEBUG) console.log('[sasjs-mock]', ...args)
32
32
  }
33
33
 
34
+ /**
35
+ * A file's contents, as text or as base64 with a flag.
36
+ *
37
+ * An upload is a file, not a string: a PNG, a workbook or an .sas7bdat read
38
+ * with `text()` arrives as replacement characters, and the drive would store
39
+ * the damage under a success status. The bytes are decoded as UTF-8 strictly -
40
+ * if that fails, they are not text by any reading a browser would accept - and
41
+ * carried as base64, which the worker turns back into the bytes the drive
42
+ * keeps.
43
+ */
44
+ const contentsOf = async (value) => {
45
+ const bytes = new Uint8Array(await value.arrayBuffer())
46
+
47
+ try {
48
+ return { csv: new TextDecoder('utf-8', { fatal: true }).decode(bytes) }
49
+ } catch (error) {
50
+ let binary = ''
51
+ const chunk = 0x8000
52
+ for (let i = 0; i < bytes.length; i += chunk)
53
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk))
54
+
55
+ return { csv: btoa(binary), binary: true }
56
+ }
57
+ }
58
+
34
59
  const readBody = async (body) => {
35
60
  const params = {}
36
61
  const tables = []
@@ -39,12 +64,16 @@
39
64
  if (typeof FormData !== 'undefined' && body instanceof FormData) {
40
65
  for (const [key, value] of body.entries()) {
41
66
  if (typeof value === 'string') params[key] = value
42
- else
67
+ else {
68
+ const contents = await contentsOf(value)
69
+
43
70
  tables.push({
44
71
  name: key,
45
72
  filename: value.name || key + '.csv',
46
- csv: await value.text()
73
+ csv: contents.csv,
74
+ ...(contents.binary ? { binary: true } : {})
47
75
  })
76
+ }
48
77
  }
49
78
  return { params, tables }
50
79
  }
@@ -574,7 +574,15 @@ const readJsonBody = async (request) => {
574
574
  name: table.name || `table${index + 1}`,
575
575
  filename:
576
576
  table.filename || `${table.name || 'table' + (index + 1)}.csv`,
577
- contents: table.csv === undefined ? '' : table.csv
577
+ // A file the bridge could not read as UTF-8 travels as base64 with a
578
+ // flag: writing its text would replace the bytes and store the damage
579
+ // under a success status, which is what a binary upload used to do.
580
+ contents:
581
+ table.csv === undefined
582
+ ? ''
583
+ : table.binary
584
+ ? toBytes(table.csv)
585
+ : table.csv
578
586
  }))
579
587
  }
580
588
  }
@@ -859,6 +867,56 @@ const routeRequest = async (request, url) => {
859
867
  try {
860
868
  await R.ensureReady()
861
869
 
870
+ // The app stream is the server's own web surface rather than part of the
871
+ // API: the listing, the landing page and the redirect from one to the other
872
+ // are routes sasjs/server answers, and an interface asks for the listing on
873
+ // every visit. Unanswered, those requests reach the static host and the page
874
+ // parses this site's index.html as JSON.
875
+ //
876
+ // The answers mirror the server's, because a caller cannot tell the two
877
+ // hosts apart otherwise: `/AppStream` redirects to `/AppStream/`, the landing
878
+ // page is served, and only a name under it is missing. The listing is empty
879
+ // rather than read from `registry()`, which records this deployment's own
880
+ // folder so the delete route has something to remove: the stream really does
881
+ // serve nothing at `/AppStream/<name>/` here, and a listing that named it
882
+ // would offer a tile that 404s.
883
+ // `path` has its trailing slashes stripped, and these four answers differ by
884
+ // exactly that, so they are decided on the request's own pathname.
885
+ const stream = url.pathname
886
+
887
+ if (/^\/AppStream$/i.test(stream))
888
+ return new Response(null, {
889
+ status: 301,
890
+ headers: { location: '/AppStream/' }
891
+ })
892
+
893
+ if (
894
+ /^\/AppStream\/$/i.test(stream) ||
895
+ /^\/AppStream\/index\.html$/i.test(stream)
896
+ )
897
+ return new Response(
898
+ [
899
+ '<!doctype html><html><head><meta charset="utf-8">',
900
+ '<title>App Stream</title></head><body>',
901
+ '<h1>SASjs App Stream</h1>',
902
+ '<p>This browser host serves no apps: nothing is published on a static site.</p>',
903
+ '</body></html>'
904
+ ].join(''),
905
+ { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } }
906
+ )
907
+
908
+ if (/^\/AppStream\/apps\.json$/i.test(stream)) return json({ apps: [] })
909
+
910
+ if (/^\/AppStream\//i.test(stream)) {
911
+ return json(
912
+ {
913
+ message:
914
+ 'This browser host serves no apps: nothing is published on a static site.'
915
+ },
916
+ 404
917
+ )
918
+ }
919
+
862
920
  // The package's own matcher, so a route is recognised however its path is
863
921
  // cased and its parameters are read the way the document states them.
864
922
  const matched = API.findRoute(request.method, path)
@@ -975,10 +1033,13 @@ self.addEventListener('fetch', (event) => {
975
1033
  return
976
1034
  }
977
1035
 
978
- // The API's own paths are answered here, including one the table does not
1036
+ // The host's own paths are answered here, including one the table does not
979
1037
  // have - falling through to the static host would put the SPA's index.html
980
- // behind a typo in a caller's URL.
981
- if (!/^\/(SASjsApi|SASLogon)(\/|$)/i.test(url.pathname)) return
1038
+ // behind a typo in a caller's URL, or behind a listing an interface parses as
1039
+ // JSON. `/AppStream` is one of those: the package serves app deployments, but
1040
+ // the listing is the server's web route, and an interface that lists apps asks
1041
+ // for it on every visit.
1042
+ if (!/^\/(SASjsApi|SASLogon|AppStream)(\/|$)/i.test(url.pathname)) return
982
1043
  event.respondWith(handle(event.request, url))
983
1044
  })
984
1045
 
@@ -472,12 +472,12 @@ var assertSafeOutputFolder = function (outputFolder, projectDir, buildFolder, we
472
472
  !isSameOrChild(buildFolder, outputFolder))
473
473
  throw refusal('that folder is outside the project');
474
474
  };
475
- var countJsPrograms = function (files) {
475
+ var countPrograms = function (files, extension) {
476
476
  return Object.keys(files).filter(function (key) {
477
477
  var inProgramFolder = PROGRAM_FOLDERS.some(function (folder) {
478
478
  return key.includes("/".concat(folder, "/"));
479
479
  });
480
- return inProgramFolder && key.toLowerCase().endsWith('.js');
480
+ return inProgramFolder && key.toLowerCase().endsWith(extension);
481
481
  }).length;
482
482
  };
483
483
  /** The script-tag attributes that change how an app's script runs. */
@@ -646,11 +646,11 @@ var patchIndexHtml = function (html, version) {
646
646
  */
647
647
  function buildMockSite(target) {
648
648
  return __awaiter(this, void 0, void 0, function () {
649
- var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
649
+ var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
650
650
  var e_8, _e;
651
- var _f, _g, _h;
652
- return __generator(this, function (_j) {
653
- switch (_j.label) {
651
+ var _f, _g, _h, _j, _k;
652
+ return __generator(this, function (_l) {
653
+ switch (_l.label) {
654
654
  case 0:
655
655
  projectDir = process.projectDir || process.cwd();
656
656
  appLocRaw = target.appLoc;
@@ -662,7 +662,7 @@ function buildMockSite(target) {
662
662
  path_1.default.join(projectDir, 'sasjsbuild');
663
663
  return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
664
664
  case 1:
665
- if (!(_j.sent())) {
665
+ if (!(_l.sent())) {
666
666
  throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
667
667
  }
668
668
  webSourceRaw = (_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.webSourcePath;
@@ -672,7 +672,7 @@ function buildMockSite(target) {
672
672
  webFolder = path_1.default.resolve(projectDir, webSourceRaw);
673
673
  return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
674
674
  case 2:
675
- if (!(_j.sent())) {
675
+ if (!(_l.sent())) {
676
676
  throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
677
677
  }
678
678
  if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
@@ -690,11 +690,24 @@ function buildMockSite(target) {
690
690
  excluded.push("/sasjs/drive/files".concat(appLoc, "/services/").concat(streamConfig.streamWebFolder, "/"));
691
691
  }
692
692
  _a = seedFromBuild(buildFolder, appLoc, excluded, [outputFolder]), seed = _a.files, skipped = _a.skipped;
693
- jsPrograms = countJsPrograms(seed);
693
+ jsPrograms = countPrograms(seed, '.js');
694
+ sasPrograms = countPrograms(seed, '.sas');
694
695
  driveEntries = Object.keys(seed).length;
695
- if (jsPrograms === 0) {
696
- throw new Error("No .js programs in ".concat(buildFolder, "/services, /jobs or /tests. This target builds SAS code, which cannot run in a browser. ") +
697
- 'Build a target whose services are JavaScript (typically a mocks project).');
696
+ // A build whose services are SAS is still a site worth having, and it is built:
697
+ // the programs are seeded on the drive, where a visitor can open them in Studio
698
+ // and read them - which is how a deployment's own backend code is inspected
699
+ // without a SAS licence and without a login. What they cannot do is run here,
700
+ // so the build says so rather than letting a Run button be the surprise.
701
+ if (jsPrograms === 0 && sasPrograms > 0) {
702
+ (_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("".concat(sasPrograms, " SAS program(s) and no JavaScript in ").concat(buildFolder, ": they are placed on the drive to read, and cannot run in a browser."));
703
+ }
704
+ // No PROGRAMS is a different thing again: a compile that produced nothing, or
705
+ // a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
706
+ // behind that is not a program folder but does reach the drive, so the count
707
+ // that decides this is of programs, not of files.
708
+ if (jsPrograms + sasPrograms === 0) {
709
+ (_j = process.logger) === null || _j === void 0 ? void 0 : _j.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
710
+ 'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
698
711
  }
699
712
  // The package supplies the drive rules the worker answers with, so an install
700
713
  // without its browser build is a build that cannot run. Checked here, with the
@@ -706,7 +719,7 @@ function buildMockSite(target) {
706
719
  'or check that @sasjs/api still publishes build/browser.js.');
707
720
  }
708
721
  if (skipped.length) {
709
- (_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("Left ".concat(skipped.length, " file(s) off the mock drive, over the ").concat(megabytes(exports.MAX_BINARY_SEED_BYTES), " per-file or ").concat(megabytes(exports.MAX_SEED_BYTES), " total the browser build seeds: ") +
722
+ (_k = process.logger) === null || _k === void 0 ? void 0 : _k.warn("Left ".concat(skipped.length, " file(s) off the mock drive, over the ").concat(megabytes(exports.MAX_BINARY_SEED_BYTES), " per-file or ").concat(megabytes(exports.MAX_SEED_BYTES), " total the browser build seeds: ") +
710
723
  "".concat(skipped
711
724
  .slice(0, 3)
712
725
  .map(function (file) {
@@ -753,7 +766,7 @@ function buildMockSite(target) {
753
766
  case 3:
754
767
  // 2. the linter, bundled from the installed packages so the code routes answer
755
768
  // with the rules this CLI resolved rather than a second implementation
756
- _j.sent();
769
+ _l.sent();
757
770
  // sw.js sits at the site root: a worker can only control the folder it is
758
771
  // served from, and the app has to be controlled from /
759
772
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -210,6 +210,32 @@ describe('page-side bridge', function () {
210
210
  }
211
211
  });
212
212
  }); });
213
+ it('carries a binary file as base64 rather than as damaged text', function () { return __awaiter(void 0, void 0, void 0, function () {
214
+ var fetchImpl, XHR, bytes, form, xhr, _a, init, payload;
215
+ return __generator(this, function (_b) {
216
+ switch (_b.label) {
217
+ case 0:
218
+ fetchImpl = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
219
+ return [2 /*return*/, jsonResponse('{"status":"success"}')];
220
+ }); }); });
221
+ XHR = loadBridge(fetchImpl).XHR;
222
+ bytes = new Uint8Array([
223
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
224
+ ]);
225
+ form = new FormData();
226
+ form.append('file', new Blob([bytes], { type: 'image/png' }), 'logo.png');
227
+ xhr = new XHR();
228
+ return [4 /*yield*/, run(xhr, 'POST', '/SASjsApi/drive/file', form)];
229
+ case 1:
230
+ _b.sent();
231
+ _a = __read(fetchImpl.mock.calls[0], 2), init = _a[1];
232
+ payload = JSON.parse(init.body);
233
+ expect(payload.tables[0].binary).toBe(true);
234
+ expect(payload.tables[0].csv).toBe(btoa('‰PNG\r\n\n'));
235
+ return [2 /*return*/];
236
+ }
237
+ });
238
+ }); });
213
239
  it('parses the response when the app asked for json', function () { return __awaiter(void 0, void 0, void 0, function () {
214
240
  var XHR, xhr;
215
241
  return __generator(this, function (_a) {
@@ -667,25 +667,121 @@ describe('browser-hosted build', function () {
667
667
  }
668
668
  });
669
669
  }); });
670
- it('refuses a target whose services are SAS, and keeps the previous site', function () { return __awaiter(void 0, void 0, void 0, function () {
671
- return __generator(this, function (_a) {
672
- switch (_a.label) {
673
- case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
674
- case 1:
675
- _a.sent();
676
- write(path_1.default.join(out(), 'stale.txt'), 'previous');
670
+ /**
671
+ * A SAS backend does not run in a browser, but the programs are still worth
672
+ * shipping: the drive holds them, so a visitor can open them in Studio and
673
+ * read what a deployment's services do. The build warns and carries on.
674
+ */
675
+ it('builds a target whose services are SAS, and warns they cannot run', function () { return __awaiter(void 0, void 0, void 0, function () {
676
+ var warn, result, seed;
677
+ var _a;
678
+ return __generator(this, function (_b) {
679
+ switch (_b.label) {
680
+ case 0:
677
681
  fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', 'services'), {
678
682
  recursive: true,
679
683
  force: true
680
684
  });
681
685
  write(path_1.default.join(root, 'sasjsbuild', 'services', 'public', 'getdata.sas'), '%put sas;\n');
682
- return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target())).rejects.toThrow(/No \.js programs/)
683
- // the failure is not allowed to take the last good site with it
686
+ warn = jest.spyOn((_a = process.logger) !== null && _a !== void 0 ? _a : console, 'warn');
687
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
688
+ case 1:
689
+ result = _b.sent();
690
+ expect(result.jsPrograms).toBe(0);
691
+ expect(result.driveEntries).toBeGreaterThan(0);
692
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('cannot run in a browser'));
693
+ seed = readSeed();
694
+ expect(Object.keys(seed.files).some(function (key) { return key.endsWith('/getdata.sas'); })).toBe(true);
695
+ warn.mockRestore();
696
+ return [2 /*return*/];
697
+ }
698
+ });
699
+ }); });
700
+ /**
701
+ * The case this build exists for: an interface with no services at all.
702
+ * sasjs/server's own frontend compiles to nothing, and the worker answers its
703
+ * API - so the target must build, ship the web app, and say out loud that the
704
+ * drive it ships is empty.
705
+ */
706
+ it('builds an interface that has no services, and says the drive is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
707
+ var _a, _b, folder, warn, result;
708
+ var e_4, _c;
709
+ var _d;
710
+ return __generator(this, function (_e) {
711
+ switch (_e.label) {
712
+ case 0:
713
+ try {
714
+ // nothing a drive could be seeded from: no services, no sync folder
715
+ for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
716
+ folder = _b.value;
717
+ fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
718
+ recursive: true,
719
+ force: true
720
+ });
721
+ }
722
+ }
723
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
724
+ finally {
725
+ try {
726
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
727
+ }
728
+ finally { if (e_4) throw e_4.error; }
729
+ }
730
+ warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
731
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
732
+ case 1:
733
+ result = _e.sent();
734
+ expect(result.jsPrograms).toBe(0);
735
+ expect(result.driveEntries).toBe(0);
736
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'index.html'))).toBe(true);
737
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('ships with an empty drive'));
738
+ warn.mockRestore();
739
+ return [2 /*return*/];
740
+ }
741
+ });
742
+ }); });
743
+ /**
744
+ * A stale build folder is the case the warning exists for: `sasjs doc` and
745
+ * other commands leave folders behind that are not program folders, so a count
746
+ * of files on the drive says nothing about whether anything was compiled.
747
+ */
748
+ it('warns when the build folder holds only folders that are not programs', function () { return __awaiter(void 0, void 0, void 0, function () {
749
+ var _a, _b, folder, warn, result;
750
+ var e_5, _c;
751
+ var _d;
752
+ return __generator(this, function (_e) {
753
+ switch (_e.label) {
754
+ case 0:
755
+ try {
756
+ for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
757
+ folder = _b.value;
758
+ fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
759
+ recursive: true,
760
+ force: true
761
+ });
762
+ }
763
+ }
764
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
765
+ finally {
766
+ try {
767
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
768
+ }
769
+ finally { if (e_5) throw e_5.error; }
770
+ }
771
+ write(path_1.default.join(root, 'sasjsbuild', 'docs', 'index.html'), '<html></html>');
772
+ warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
773
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
774
+ // the drive is NOT empty - the docs folder reaches it - so the file count
775
+ // alone would have kept quiet
684
776
  ];
685
- case 2:
686
- _a.sent();
687
- // the failure is not allowed to take the last good site with it
688
- expect(fs_1.default.readFileSync(path_1.default.join(out(), 'stale.txt'), 'utf8')).toBe('previous');
777
+ case 1:
778
+ result = _e.sent();
779
+ // the drive is NOT empty - the docs folder reaches it - so the file count
780
+ // alone would have kept quiet
781
+ expect(result.driveEntries).toBeGreaterThan(0);
782
+ expect(result.jsPrograms).toBe(0);
783
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('No compiled programs'));
784
+ warn.mockRestore();
689
785
  return [2 /*return*/];
690
786
  }
691
787
  });
@@ -773,8 +869,8 @@ describe('browser-hosted build', function () {
773
869
  });
774
870
  }); });
775
871
  it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
776
- var _a, _b, mockWebFolder, e_4_1;
777
- var e_4, _c;
872
+ var _a, _b, mockWebFolder, e_6_1;
873
+ var e_6, _c;
778
874
  return __generator(this, function (_d) {
779
875
  switch (_d.label) {
780
876
  case 0:
@@ -805,14 +901,14 @@ describe('browser-hosted build', function () {
805
901
  return [3 /*break*/, 1];
806
902
  case 4: return [3 /*break*/, 7];
807
903
  case 5:
808
- e_4_1 = _d.sent();
809
- e_4 = { error: e_4_1 };
904
+ e_6_1 = _d.sent();
905
+ e_6 = { error: e_6_1 };
810
906
  return [3 /*break*/, 7];
811
907
  case 6:
812
908
  try {
813
909
  if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
814
910
  }
815
- finally { if (e_4) throw e_4.error; }
911
+ finally { if (e_6) throw e_6.error; }
816
912
  return [7 /*endfinally*/];
817
913
  case 7:
818
914
  expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
@@ -1139,6 +1139,46 @@ describe('service worker host', function () {
1139
1139
  });
1140
1140
  }); });
1141
1141
  });
1142
+ it('writes a binary upload as its bytes, not as the text it decodes to', function () { return __awaiter(void 0, void 0, void 0, function () {
1143
+ var dispatch, image, created, read, _a, _b;
1144
+ return __generator(this, function (_c) {
1145
+ switch (_c.label) {
1146
+ case 0:
1147
+ dispatch = seeded().dispatch;
1148
+ image = new Uint8Array([
1149
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
1150
+ ]);
1151
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file', {
1152
+ method: 'POST',
1153
+ headers: { 'content-type': 'application/json' },
1154
+ body: JSON.stringify({
1155
+ params: { filePath: '/Public/logo.png' },
1156
+ tables: [
1157
+ {
1158
+ name: 'file',
1159
+ filename: 'logo.png',
1160
+ csv: btoa(String.fromCharCode.apply(null, Array.from(image))),
1161
+ binary: true
1162
+ }
1163
+ ]
1164
+ })
1165
+ })];
1166
+ case 1:
1167
+ created = _c.sent();
1168
+ expect(created.status).toBe(200);
1169
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/Public/logo.png'))];
1170
+ case 2:
1171
+ read = _c.sent();
1172
+ expect(read.status).toBe(200);
1173
+ _a = expect;
1174
+ _b = Uint8Array.bind;
1175
+ return [4 /*yield*/, read.arrayBuffer()];
1176
+ case 3:
1177
+ _a.apply(void 0, [new (_b.apply(Uint8Array, [void 0, _c.sent()]))()]).toEqual(image);
1178
+ return [2 /*return*/];
1179
+ }
1180
+ });
1181
+ }); });
1142
1182
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
1143
1183
  var dispatch, response, _a;
1144
1184
  return __generator(this, function (_b) {
@@ -1157,6 +1197,46 @@ describe('service worker host', function () {
1157
1197
  }
1158
1198
  });
1159
1199
  }); });
1200
+ it('answers the app stream as the server does, with nothing in it', function () { return __awaiter(void 0, void 0, void 0, function () {
1201
+ var dispatch, listing, _a, root, landing, _b, app, _c;
1202
+ return __generator(this, function (_d) {
1203
+ switch (_d.label) {
1204
+ case 0:
1205
+ dispatch = seeded().dispatch;
1206
+ return [4 /*yield*/, dispatch('/AppStream/apps.json')];
1207
+ case 1:
1208
+ listing = _d.sent();
1209
+ expect(listing.status).toBe(200);
1210
+ _a = expect;
1211
+ return [4 /*yield*/, listing.json()];
1212
+ case 2:
1213
+ _a.apply(void 0, [_d.sent()]).toEqual({ apps: [] });
1214
+ return [4 /*yield*/, dispatch('/AppStream')];
1215
+ case 3:
1216
+ root = _d.sent();
1217
+ expect(root.status).toBe(301);
1218
+ expect(root.headers.get('location')).toBe('/AppStream/');
1219
+ return [4 /*yield*/, dispatch('/AppStream/')];
1220
+ case 4:
1221
+ landing = _d.sent();
1222
+ expect(landing.status).toBe(200);
1223
+ expect(landing.headers.get('content-type')).toContain('text/html');
1224
+ _b = expect;
1225
+ return [4 /*yield*/, landing.text()];
1226
+ case 5:
1227
+ _b.apply(void 0, [_d.sent()]).toContain('serves no apps');
1228
+ return [4 /*yield*/, dispatch('/AppStream/Demo/')];
1229
+ case 6:
1230
+ app = _d.sent();
1231
+ expect(app.status).toBe(404);
1232
+ _c = expect;
1233
+ return [4 /*yield*/, app.json()];
1234
+ case 7:
1235
+ _c.apply(void 0, [(_d.sent()).message]).toContain('serves no apps');
1236
+ return [2 /*return*/];
1237
+ }
1238
+ });
1239
+ }); });
1160
1240
  it('leaves a request to another origin alone', function () {
1161
1241
  var dispatch = seeded().dispatch;
1162
1242
  expect(dispatch('https://other.example.com/SASjsApi/info')).toBeUndefined();
package/package.json CHANGED
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.16.0"
128
+ "version": "6.18.0"
129
129
  }