@sasjs/cli 6.19.2 → 6.19.3
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/build/commands/build/internal/mockSite/assets/persist.js +85 -0
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +17 -1
- package/build/commands/build/internal/mockSite/mockSite.js +8 -1
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +28 -0
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +204 -1
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +50 -0
- package/package.json +1 -1
|
@@ -89,6 +89,63 @@
|
|
|
89
89
|
|
|
90
90
|
const clearAll = () => withStore('readwrite', (store) => store.clear())
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The build this site is, as the page can see it: mock-config.json carries the
|
|
94
|
+
* stamp the build wrote over its host code and the seed it lays out. Read here
|
|
95
|
+
* rather than passed down, because the page decides whether to keep a stored
|
|
96
|
+
* drive before the worker is asked anything.
|
|
97
|
+
*
|
|
98
|
+
* The base is the worker's script URL, not the page's: the site can be served
|
|
99
|
+
* from a folder, and a page reached at `/demo` (no trailing slash) has a
|
|
100
|
+
* baseURI of `https://host/demo`, which resolves `host/mock-config.json` one
|
|
101
|
+
* level too high. The worker sits at `<base>/sw.js` and fetches the same file
|
|
102
|
+
* relative to itself, so its URL is the one definition of the site folder.
|
|
103
|
+
*
|
|
104
|
+
* Empty when the config cannot be read, which leaves the drive alone: a page
|
|
105
|
+
* that cannot tell which build it is has no business dropping one.
|
|
106
|
+
*/
|
|
107
|
+
const readBuildStamp = async (controller) => {
|
|
108
|
+
try {
|
|
109
|
+
const script = controller && controller.scriptURL
|
|
110
|
+
// no controller URL means no way to name the site folder; keep the drive
|
|
111
|
+
if (!script) return ''
|
|
112
|
+
const res = await fetch(new URL('host/mock-config.json', script), {
|
|
113
|
+
cache: 'no-store'
|
|
114
|
+
})
|
|
115
|
+
if (!res.ok) return ''
|
|
116
|
+
const config = await res.json()
|
|
117
|
+
return config && typeof config.buildStamp === 'string'
|
|
118
|
+
? config.buildStamp
|
|
119
|
+
: ''
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return ''
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Where the stamp of the build that wrote the stored drive is kept. */
|
|
126
|
+
const BUILD_STAMP_KEY = 'sasjs-mock-build'
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The stamp recorded for the stored drive, '' when there is none (a drive
|
|
130
|
+
* from a build before stamps existed) and null when storage cannot be read at
|
|
131
|
+
* all - which is not the same answer, and must not drop a drive on every load.
|
|
132
|
+
*/
|
|
133
|
+
const storedBuildStamp = () => {
|
|
134
|
+
try {
|
|
135
|
+
return localStorage.getItem(BUILD_STAMP_KEY) || ''
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const keepBuildStamp = (stamp) => {
|
|
142
|
+
try {
|
|
143
|
+
if (stamp) localStorage.setItem(BUILD_STAMP_KEY, stamp)
|
|
144
|
+
} catch (e) {
|
|
145
|
+
/* a browser that refuses storage still runs the site */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
92
149
|
const waitForController = () =>
|
|
93
150
|
new Promise((resolve) => {
|
|
94
151
|
if (navigator.serviceWorker.controller)
|
|
@@ -210,6 +267,32 @@
|
|
|
210
267
|
console.error('[sasjs-mock] could not read storage: ' + e)
|
|
211
268
|
}
|
|
212
269
|
|
|
270
|
+
// A stored drive outlives the build that wrote it, so a returning visitor
|
|
271
|
+
// would keep running that build's services - including ones the app being
|
|
272
|
+
// loaded no longer matches. This build stamps the seed it ships, and a drive
|
|
273
|
+
// whose stamp is absent or different is dropped rather than merged: a
|
|
274
|
+
// partial drive from two builds is the one thing that cannot be reasoned
|
|
275
|
+
// about. A site whose config carries no stamp keeps its drives, which is the
|
|
276
|
+
// behaviour every build before this one had.
|
|
277
|
+
const stamp = await readBuildStamp(controller)
|
|
278
|
+
const previous = storedBuildStamp()
|
|
279
|
+
if (
|
|
280
|
+
Object.keys(files).length &&
|
|
281
|
+
stamp &&
|
|
282
|
+
previous !== null &&
|
|
283
|
+
previous !== stamp
|
|
284
|
+
) {
|
|
285
|
+
console.info(
|
|
286
|
+
'[sasjs-mock] the stored drive is from another build - seeding this one'
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
await clearAll()
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.error('[sasjs-mock] could not clear storage: ' + e)
|
|
292
|
+
}
|
|
293
|
+
files = {}
|
|
294
|
+
}
|
|
295
|
+
|
|
213
296
|
if (Object.keys(files).length) {
|
|
214
297
|
window.__sasjsMockBoot = await askWorker(
|
|
215
298
|
{ type: 'sasjs-load', files },
|
|
@@ -221,6 +304,8 @@
|
|
|
221
304
|
'sasjs-seed-ack'
|
|
222
305
|
)
|
|
223
306
|
}
|
|
307
|
+
if (window.__sasjsMockBoot && !window.__sasjsMockBoot.error)
|
|
308
|
+
keepBuildStamp(stamp)
|
|
224
309
|
return window.__sasjsMockBoot
|
|
225
310
|
})()
|
|
226
311
|
})()
|
|
@@ -1100,7 +1100,16 @@ return _webout;
|
|
|
1100
1100
|
BUFFERS
|
|
1101
1101
|
)
|
|
1102
1102
|
} catch (e) {
|
|
1103
|
-
|
|
1103
|
+
// The stack alone is not an error message: WebKit's carries frames only, so
|
|
1104
|
+
// a failure there arrives at the app as "anonymous@... > Function:219:1" with
|
|
1105
|
+
// the reason nowhere in it. V8's stack already opens with the message, so it
|
|
1106
|
+
// is prepended only when it is missing rather than repeated.
|
|
1107
|
+
const message = e && e.message ? String(e.message) : ''
|
|
1108
|
+
const stack = e && e.stack ? String(e.stack) : ''
|
|
1109
|
+
thrown =
|
|
1110
|
+
stack && message && stack.indexOf(message) < 0
|
|
1111
|
+
? message + '\n' + stack
|
|
1112
|
+
: stack || String(e)
|
|
1104
1113
|
logLines.push(String(thrown))
|
|
1105
1114
|
}
|
|
1106
1115
|
|
|
@@ -1415,6 +1424,13 @@ const boot = async () => {
|
|
|
1415
1424
|
await loadMockConfig()
|
|
1416
1425
|
await loadWeboutPrelude()
|
|
1417
1426
|
const { source, files: seed } = await fetchDrive()
|
|
1427
|
+
// a seed replaces the drive, it does not merge into one: the worker survives
|
|
1428
|
+
// page loads, so it can still hold the drive it was given last time, and any
|
|
1429
|
+
// file only that drive had would otherwise outlive the build that wrote it
|
|
1430
|
+
FILES.clear()
|
|
1431
|
+
SIZES.clear()
|
|
1432
|
+
DIRS.clear()
|
|
1433
|
+
DIRS.add('/')
|
|
1418
1434
|
mkdirp(DRIVE_APP)
|
|
1419
1435
|
mkdirp(`${DRIVE_ROOT}/files`)
|
|
1420
1436
|
for (const [path, contents] of Object.entries(seed)) {
|
|
@@ -683,7 +683,7 @@ var patchIndexHtml = function (html, version, basePath) {
|
|
|
683
683
|
*/
|
|
684
684
|
function buildMockSite(target) {
|
|
685
685
|
return __awaiter(this, void 0, void 0, function () {
|
|
686
|
-
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
686
|
+
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
687
687
|
var e_8, _e;
|
|
688
688
|
var _f, _g, _h, _j, _k, _l;
|
|
689
689
|
return __generator(this, function (_m) {
|
|
@@ -837,6 +837,12 @@ function buildMockSite(target) {
|
|
|
837
837
|
.reduce(function (all, buf) { return Buffer.concat([all, buf]); }, Buffer.alloc(0)))
|
|
838
838
|
.digest('hex')
|
|
839
839
|
.slice(0, 10);
|
|
840
|
+
buildStamp = (0, crypto_1.createHash)('md5')
|
|
841
|
+
.update(JSON.stringify(Object.keys(seed)
|
|
842
|
+
.sort()
|
|
843
|
+
.map(function (key) { return [key, seed[key]]; })))
|
|
844
|
+
.digest('hex')
|
|
845
|
+
.slice(0, 10);
|
|
840
846
|
swPath = path_1.default.join(outputFolder, 'sw.js');
|
|
841
847
|
fs_1.default.writeFileSync(swPath, fs_1.default
|
|
842
848
|
.readFileSync(swPath, 'utf8')
|
|
@@ -857,6 +863,7 @@ function buildMockSite(target) {
|
|
|
857
863
|
jsPrograms: jsPrograms,
|
|
858
864
|
programFolders: PROGRAM_FOLDERS,
|
|
859
865
|
snapshot: hasSnapshot,
|
|
866
|
+
buildStamp: buildStamp,
|
|
860
867
|
builtAt: new Date().toISOString()
|
|
861
868
|
}, null, 2));
|
|
862
869
|
indexPath = path_1.default.join(outputFolder, 'index.html');
|
|
@@ -136,6 +136,34 @@ describe('browser-hosted build', function () {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
}); });
|
|
139
|
+
it('stamps the config with the build, so a page can tell one drive from another', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
|
+
var first;
|
|
141
|
+
return __generator(this, function (_a) {
|
|
142
|
+
switch (_a.label) {
|
|
143
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
144
|
+
case 1:
|
|
145
|
+
_a.sent();
|
|
146
|
+
first = readConfig();
|
|
147
|
+
expect(first.buildStamp).toMatch(/^[0-9a-f]{10}$/);
|
|
148
|
+
// an identical build keeps its stamp: a rebuild that changes nothing must
|
|
149
|
+
// not throw a returning visitor's drive away
|
|
150
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
151
|
+
case 2:
|
|
152
|
+
// an identical build keeps its stamp: a rebuild that changes nothing must
|
|
153
|
+
// not throw a returning visitor's drive away
|
|
154
|
+
_a.sent();
|
|
155
|
+
expect(readConfig().buildStamp).toBe(first.buildStamp);
|
|
156
|
+
// a changed service changes the stamp, which is what makes the page re-seed
|
|
157
|
+
// rather than run the previous build's program
|
|
158
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'services', 'public', 'getdata.js'), 'const a = 999\n');
|
|
159
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
160
|
+
case 3:
|
|
161
|
+
_a.sent();
|
|
162
|
+
expect(readConfig().buildStamp).not.toBe(first.buildStamp);
|
|
163
|
+
return [2 /*return*/];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}); });
|
|
139
167
|
it('stamps the seed with the drive format the runtime checks', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
168
|
return __generator(this, function (_a) {
|
|
141
169
|
switch (_a.label) {
|
|
@@ -137,6 +137,10 @@ var loadPersist = function (options) {
|
|
|
137
137
|
var controller = options.controller === undefined
|
|
138
138
|
? { postMessage: function (message) { return posted.push(message); } }
|
|
139
139
|
: options.controller;
|
|
140
|
+
// a real controller names the site folder, which is how the page finds the
|
|
141
|
+
// config file; every case gets one unless it is testing that it is missing
|
|
142
|
+
if (controller && !controller.scriptURL)
|
|
143
|
+
controller.scriptURL = options.scriptURL || 'https://host.test/sw.js';
|
|
140
144
|
var serviceWorker = {
|
|
141
145
|
controller: controller,
|
|
142
146
|
// Never resolves by default: the boot sequence then stops before it touches
|
|
@@ -158,6 +162,36 @@ var loadPersist = function (options) {
|
|
|
158
162
|
var window = {};
|
|
159
163
|
global.window = window;
|
|
160
164
|
global.navigator = { serviceWorker: serviceWorker };
|
|
165
|
+
// the page reads its build stamp and what the stored drive was written by
|
|
166
|
+
var local = new Map();
|
|
167
|
+
if (options.storedStamp !== undefined)
|
|
168
|
+
local.set('sasjs-mock-build', options.storedStamp);
|
|
169
|
+
global.localStorage = options.storageUnavailable
|
|
170
|
+
? {
|
|
171
|
+
getItem: function () {
|
|
172
|
+
throw new Error('storage is denied');
|
|
173
|
+
},
|
|
174
|
+
setItem: function () {
|
|
175
|
+
throw new Error('storage is denied');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
getItem: function (key) { return (local.has(key) ? local.get(key) : null); },
|
|
180
|
+
setItem: function (key, value) { return local.set(key, value); }
|
|
181
|
+
};
|
|
182
|
+
global.document = {
|
|
183
|
+
baseURI: options.baseURI || 'https://host.test/'
|
|
184
|
+
};
|
|
185
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
186
|
+
return __generator(this, function (_a) {
|
|
187
|
+
return [2 /*return*/, ({
|
|
188
|
+
ok: true,
|
|
189
|
+
json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
190
|
+
return [2 /*return*/, ({ buildStamp: options.buildStamp || '' })];
|
|
191
|
+
}); }); }
|
|
192
|
+
})];
|
|
193
|
+
});
|
|
194
|
+
}); });
|
|
161
195
|
var idb = options.seeded
|
|
162
196
|
? (function () {
|
|
163
197
|
var fake = createIndexedDB();
|
|
@@ -171,7 +205,15 @@ var loadPersist = function (options) {
|
|
|
171
205
|
global.indexedDB = idb;
|
|
172
206
|
require('../assets/persist.js');
|
|
173
207
|
var reply = function (data) { return __spreadArray([], __read(listeners), false).forEach(function (fn) { return fn({ data: data }); }); };
|
|
174
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
window: window,
|
|
210
|
+
listeners: listeners,
|
|
211
|
+
removed: removed,
|
|
212
|
+
posted: posted,
|
|
213
|
+
reply: reply,
|
|
214
|
+
rows: idb.rows,
|
|
215
|
+
stored: local
|
|
216
|
+
};
|
|
175
217
|
};
|
|
176
218
|
describe('page-side store', function () {
|
|
177
219
|
afterEach(function () {
|
|
@@ -179,6 +221,9 @@ describe('page-side store', function () {
|
|
|
179
221
|
delete global.window;
|
|
180
222
|
delete global.navigator;
|
|
181
223
|
delete global.indexedDB;
|
|
224
|
+
delete global.document;
|
|
225
|
+
delete global.localStorage;
|
|
226
|
+
delete global.fetch;
|
|
182
227
|
});
|
|
183
228
|
it('asks the worker for a snapshot and drops the listener when it replies', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
184
229
|
var _a, window, listeners, removed, reply, before, pending;
|
|
@@ -473,4 +518,162 @@ describe('page-side store', function () {
|
|
|
473
518
|
}
|
|
474
519
|
});
|
|
475
520
|
}); });
|
|
521
|
+
/**
|
|
522
|
+
* Answers whatever the page asks, so a boot test can assert on which question
|
|
523
|
+
* was asked rather than on the reply.
|
|
524
|
+
*/
|
|
525
|
+
var answerBoot = function (controller, listeners) {
|
|
526
|
+
var answering = setInterval(function () {
|
|
527
|
+
var asked = controller.postMessage.mock.calls;
|
|
528
|
+
if (!asked.length)
|
|
529
|
+
return;
|
|
530
|
+
var type = asked[0][0].type;
|
|
531
|
+
__spreadArray([], __read(listeners), false).forEach(function (fn) { return fn({ data: { type: type + '-ack' } }); });
|
|
532
|
+
}, 5);
|
|
533
|
+
return function () { return clearInterval(answering); };
|
|
534
|
+
};
|
|
535
|
+
it("drops a drive written by another build and takes this build's seed", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
536
|
+
var controller, _a, window, listeners, stored, rows, stop;
|
|
537
|
+
return __generator(this, function (_b) {
|
|
538
|
+
switch (_b.label) {
|
|
539
|
+
case 0:
|
|
540
|
+
controller = { postMessage: jest.fn() };
|
|
541
|
+
_a = loadPersist({
|
|
542
|
+
controller: controller,
|
|
543
|
+
ready: Promise.resolve({}),
|
|
544
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
545
|
+
buildStamp: 'build-b',
|
|
546
|
+
storedStamp: 'build-a'
|
|
547
|
+
}), window = _a.window, listeners = _a.listeners, stored = _a.stored, rows = _a.rows;
|
|
548
|
+
stop = answerBoot(controller, listeners);
|
|
549
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
550
|
+
case 1:
|
|
551
|
+
_b.sent();
|
|
552
|
+
stop();
|
|
553
|
+
expect(controller.postMessage).toHaveBeenCalledWith({ type: 'sasjs-seed' });
|
|
554
|
+
expect(stored.get('sasjs-mock-build')).toBe('build-b');
|
|
555
|
+
// the old drive is gone rather than merged with the new seed
|
|
556
|
+
expect(rows.size).toBe(0);
|
|
557
|
+
return [2 /*return*/];
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}); });
|
|
561
|
+
it('seeds over a drive from a build before the stamp existed', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
562
|
+
var controller, _a, window, listeners, stop;
|
|
563
|
+
return __generator(this, function (_b) {
|
|
564
|
+
switch (_b.label) {
|
|
565
|
+
case 0:
|
|
566
|
+
controller = { postMessage: jest.fn() };
|
|
567
|
+
_a = loadPersist({
|
|
568
|
+
controller: controller,
|
|
569
|
+
ready: Promise.resolve({}),
|
|
570
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
571
|
+
buildStamp: 'build-b'
|
|
572
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
573
|
+
stop = answerBoot(controller, listeners);
|
|
574
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
575
|
+
case 1:
|
|
576
|
+
_b.sent();
|
|
577
|
+
stop();
|
|
578
|
+
expect(controller.postMessage).toHaveBeenCalledWith({ type: 'sasjs-seed' });
|
|
579
|
+
return [2 /*return*/];
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}); });
|
|
583
|
+
it('keeps a drive this build wrote', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
584
|
+
var controller, _a, window, listeners, stop;
|
|
585
|
+
return __generator(this, function (_b) {
|
|
586
|
+
switch (_b.label) {
|
|
587
|
+
case 0:
|
|
588
|
+
controller = { postMessage: jest.fn() };
|
|
589
|
+
_a = loadPersist({
|
|
590
|
+
controller: controller,
|
|
591
|
+
ready: Promise.resolve({}),
|
|
592
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
593
|
+
buildStamp: 'build-b',
|
|
594
|
+
storedStamp: 'build-b'
|
|
595
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
596
|
+
stop = answerBoot(controller, listeners);
|
|
597
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
598
|
+
case 1:
|
|
599
|
+
_b.sent();
|
|
600
|
+
stop();
|
|
601
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'sasjs-load' }));
|
|
602
|
+
return [2 /*return*/];
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}); });
|
|
606
|
+
it('keeps the drive when the site publishes no stamp', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
607
|
+
var controller, _a, window, listeners, stop;
|
|
608
|
+
return __generator(this, function (_b) {
|
|
609
|
+
switch (_b.label) {
|
|
610
|
+
case 0:
|
|
611
|
+
controller = { postMessage: jest.fn() };
|
|
612
|
+
_a = loadPersist({
|
|
613
|
+
controller: controller,
|
|
614
|
+
ready: Promise.resolve({}),
|
|
615
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' }
|
|
616
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
617
|
+
stop = answerBoot(controller, listeners);
|
|
618
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
619
|
+
case 1:
|
|
620
|
+
_b.sent();
|
|
621
|
+
stop();
|
|
622
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'sasjs-load' }));
|
|
623
|
+
return [2 /*return*/];
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}); });
|
|
627
|
+
it('keeps the drive when storage is refused, instead of seeding every load', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
628
|
+
var controller, _a, window, listeners, stop;
|
|
629
|
+
return __generator(this, function (_b) {
|
|
630
|
+
switch (_b.label) {
|
|
631
|
+
case 0:
|
|
632
|
+
controller = { postMessage: jest.fn() };
|
|
633
|
+
_a = loadPersist({
|
|
634
|
+
controller: controller,
|
|
635
|
+
ready: Promise.resolve({}),
|
|
636
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
637
|
+
buildStamp: 'build-b',
|
|
638
|
+
storageUnavailable: true
|
|
639
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
640
|
+
stop = answerBoot(controller, listeners);
|
|
641
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
642
|
+
case 1:
|
|
643
|
+
_b.sent();
|
|
644
|
+
stop();
|
|
645
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'sasjs-load' }));
|
|
646
|
+
return [2 /*return*/];
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}); });
|
|
650
|
+
it('reads the config from the folder the worker sits in, not the page URL', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
651
|
+
var controller, _a, window, listeners, stop, _b, url, init;
|
|
652
|
+
return __generator(this, function (_c) {
|
|
653
|
+
switch (_c.label) {
|
|
654
|
+
case 0:
|
|
655
|
+
controller = { postMessage: jest.fn() };
|
|
656
|
+
_a = loadPersist({
|
|
657
|
+
controller: controller,
|
|
658
|
+
ready: Promise.resolve({}),
|
|
659
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
660
|
+
buildStamp: 'build-b',
|
|
661
|
+
storedStamp: 'build-a',
|
|
662
|
+
scriptURL: 'https://host.test/demo/sw.js',
|
|
663
|
+
baseURI: 'https://host.test/demo'
|
|
664
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
665
|
+
stop = answerBoot(controller, listeners);
|
|
666
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
667
|
+
case 1:
|
|
668
|
+
_c.sent();
|
|
669
|
+
stop();
|
|
670
|
+
_b = __read(global.fetch.mock.calls[0], 2), url = _b[0], init = _b[1];
|
|
671
|
+
expect(String(url)).toBe('https://host.test/demo/host/mock-config.json');
|
|
672
|
+
expect(init).toEqual({ cache: 'no-store' });
|
|
673
|
+
// and the stamp it read is acted on
|
|
674
|
+
expect(controller.postMessage).toHaveBeenCalledWith({ type: 'sasjs-seed' });
|
|
675
|
+
return [2 /*return*/];
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}); });
|
|
476
679
|
});
|
|
@@ -136,6 +136,37 @@ describe('browser runtime', function () {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
}); });
|
|
139
|
+
it('replaces the drive when a seed arrives, rather than merging the two', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
|
+
var runtime, info;
|
|
141
|
+
var _a;
|
|
142
|
+
return __generator(this, function (_b) {
|
|
143
|
+
switch (_b.label) {
|
|
144
|
+
case 0:
|
|
145
|
+
runtime = loadRuntime();
|
|
146
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {}, _a[drivePath('first.txt')] = 'one', _a))
|
|
147
|
+
// a worker outlives the page that seeded it, so a later seed - a page that
|
|
148
|
+
// found a drive from another build, or a reset - arrives with the previous
|
|
149
|
+
// drive still in memory. A file only that drive had must not survive it.
|
|
150
|
+
];
|
|
151
|
+
case 1:
|
|
152
|
+
_b.sent();
|
|
153
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
154
|
+
var _a;
|
|
155
|
+
return __generator(this, function (_b) {
|
|
156
|
+
return [2 /*return*/, url.includes('mock-config')
|
|
157
|
+
? jsonResponse({ appLoc: appLoc })
|
|
158
|
+
: jsonResponse({ files: (_a = {}, _a[drivePath('second.txt')] = 'two', _a) })];
|
|
159
|
+
});
|
|
160
|
+
}); });
|
|
161
|
+
return [4 /*yield*/, runtime.seed()];
|
|
162
|
+
case 2:
|
|
163
|
+
info = _b.sent();
|
|
164
|
+
expect(info.files).toBe(1);
|
|
165
|
+
expect(Object.keys(runtime.dumpAll())).toEqual([drivePath('second.txt')]);
|
|
166
|
+
return [2 /*return*/];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}); });
|
|
139
170
|
it('reads a drive file with no format stamp as the current format', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
171
|
var runtime;
|
|
141
172
|
var _a;
|
|
@@ -294,6 +325,25 @@ describe('browser runtime', function () {
|
|
|
294
325
|
expect(result.log).toContain('boom');
|
|
295
326
|
expect(result.webout).toBe('');
|
|
296
327
|
});
|
|
328
|
+
it('carries the message when the stack does not, as WebKit reports one', function () {
|
|
329
|
+
var runtime = loadRuntime();
|
|
330
|
+
// WebKit's Error.stack is frames only - V8's opens with the message - so the
|
|
331
|
+
// body a service failure returns had the reason nowhere in it there, which is
|
|
332
|
+
// what a stack of "Function:219:1" in an app's error dialog looks like.
|
|
333
|
+
var result = runtime.run({
|
|
334
|
+
code: [
|
|
335
|
+
'const error = new Error("webout is not defined")',
|
|
336
|
+
'error.stack = "global code@https://host.test/sw.js:1:1"',
|
|
337
|
+
'throw error'
|
|
338
|
+
].join('\n')
|
|
339
|
+
});
|
|
340
|
+
expect(result.error).toBe('webout is not defined\nglobal code@https://host.test/sw.js:1:1');
|
|
341
|
+
});
|
|
342
|
+
it('does not repeat a message the stack already carries', function () {
|
|
343
|
+
var runtime = loadRuntime();
|
|
344
|
+
var result = runtime.run({ code: 'throw new Error("boom")' });
|
|
345
|
+
expect(result.error.match(/boom/g)).toHaveLength(1);
|
|
346
|
+
});
|
|
297
347
|
/**
|
|
298
348
|
* The shims a service reaches for are node's, refusals included: a mock that
|
|
299
349
|
* answered a copy from a missing file with an empty file, or accepted
|
package/package.json
CHANGED