frugal-iot-client 2.0.2 → 2.0.4

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/test/page.test.js CHANGED
@@ -84,7 +84,7 @@ describe('which admin cards a user gets', () => {
84
84
  assert.deepEqual(cards.adminCardsFor('dev').map((c) => c.section), ['info', 'flash']);
85
85
  withCapabilities('READ', 'ADMIN');
86
86
  assert.deepEqual(cards.adminCardsFor('dev').map((c) => c.section),
87
- ['info', 'admin', 'projects', 'message', 'retained', 'nodes', 'api']);
87
+ ['info', 'admin', 'projects', 'message', 'retained', 'nodes', 'bridges', 'api']);
88
88
  });
89
89
 
90
90
  test('flashing and pushing an OTA binary are separate capabilities', () => {
@@ -108,7 +108,7 @@ describe('the project back', () => {
108
108
  const back = document.createElement('mqtt-projectback');
109
109
  back.setAttribute('organization', 'dev');
110
110
  document.body.append(back);
111
- assert.equal(back.querySelectorAll('.fi-admincard').length, 7);
111
+ assert.equal(back.querySelectorAll('.fi-admincard').length, 8);
112
112
  back.remove();
113
113
  });
114
114
 
@@ -228,11 +228,129 @@ describe('the project back', () => {
228
228
  document.body.append(back);
229
229
  back.querySelectorAll('.fi-admincard__head').forEach((h) => h.click());
230
230
  const sections = [...back.querySelectorAll('mqtt-admin')].map((a) => a.getAttribute('section'));
231
- assert.deepEqual(sections, ['admin', 'projects', 'message', 'retained', 'nodes', 'api']);
231
+ assert.deepEqual(sections, ['admin', 'projects', 'message', 'retained', 'nodes', 'bridges', 'api']);
232
232
  back.remove();
233
233
  });
234
234
  });
235
235
 
236
+ // The bolt is in the OTA card and the flasher is a card of its own, so this is the one path between
237
+ // two mqtt-admin elements that cannot be reached by either on its own.
238
+ describe('the bolt beside an OTA file', () => {
239
+ // /ota_get is a real request the jsdom tests cannot make; record it instead
240
+ function withFetchRecorded(fn) {
241
+ const asked = [];
242
+ const real = globalThis.fetch;
243
+ globalThis.fetch = (url) => { asked.push(url); return Promise.resolve({ok: true, arrayBuffer: () => Promise.resolve(new ArrayBuffer(4))}); };
244
+ try { fn(asked); } finally { globalThis.fetch = real; }
245
+ }
246
+ function backWithOtaFile(file) {
247
+ const back = document.createElement('mqtt-projectback');
248
+ back.setAttribute('organization', 'dev');
249
+ document.body.append(back);
250
+ back.state.elements.ota.querySelector('.fi-admincard__head').click();
251
+ const otaAdmin = back.state.elements.ota.querySelector('mqtt-admin');
252
+ otaAdmin.state.ota_files = [file]; // what /ota_list would have said
253
+ otaAdmin.replaceElement('ota_files', otaAdmin.otaFilesList());
254
+ return {back, otaAdmin};
255
+ }
256
+
257
+ test('opens the Flash card and hands it the file', () => {
258
+ withCapabilities('OTAUPDATE', 'OTAFLASH');
259
+ const {back, otaAdmin} = backWithOtaFile('esp32/lotus/1.0.0');
260
+ withFetchRecorded((asked) => {
261
+ otaAdmin.shadowRoot.querySelector('.otaicon').click(); // the bolt, not a handler call
262
+ assert.ok(back.state.elements.flash.classList.contains('fi-admincard--open'), 'Flash stayed shut');
263
+ assert.deepEqual(asked, ['/ota_get/dev/esp32/lotus/1.0.0']);
264
+ });
265
+ back.remove();
266
+ });
267
+
268
+ test('is not offered without OTAFLASH, since there is then no Flash card to open', () => {
269
+ withCapabilities('OTAUPDATE');
270
+ const {back, otaAdmin} = backWithOtaFile('esp32/lotus/1.0.0');
271
+ assert.equal(otaAdmin.shadowRoot.querySelector('.otaicon[title]').textContent, '\u{1f5d1}',
272
+ 'the only icon should be the basket');
273
+ back.remove();
274
+ });
275
+
276
+ test('the name downloads rather than deleting - a mis-tap cost a file', () => {
277
+ withCapabilities('OTAUPDATE');
278
+ const {back, otaAdmin} = backWithOtaFile('esp32/lotus/1.0.0');
279
+ const link = otaAdmin.shadowRoot.querySelector('a[download]');
280
+ assert.ok(link, 'the file name should be a download link');
281
+ assert.equal(link.textContent, 'esp32/lotus/1.0.0');
282
+ assert.equal(link.getAttribute('href'), '/ota_get/dev/esp32/lotus/1.0.0');
283
+ back.remove();
284
+ });
285
+ });
286
+
287
+ // The server answers an OTA upload with a redirect, so a plain form post landed the whole page on
288
+ // /dashboard/ - no organization chosen, and the admin cards shut.
289
+ describe('uploading an OTA binary', () => {
290
+ // Every request the card makes: the list it reads on opening, the upload, and the re-read after.
291
+ // Node's Request wants an absolute URL, where a browser resolves one against the page, so GET()
292
+ // cannot reach the stub without standing in for that too.
293
+ function withFetch(fn) {
294
+ const asked = [];
295
+ const real = {fetch: globalThis.fetch, Request: globalThis.Request};
296
+ globalThis.Request = class { constructor(url, init) { Object.assign(this, init); this.url = String(url); } };
297
+ globalThis.fetch = (req, opts) => {
298
+ const url = (req && req.url) || String(req);
299
+ asked.push({url, method: (opts && opts.method) || (req && req.method) || 'GET'});
300
+ if (url.startsWith('/ota_list')) return Promise.resolve({
301
+ ok: true, status: 200, url,
302
+ headers: {get: () => 'application/json'},
303
+ json: () => Promise.resolve(['esp32/lotus/1.0.0']),
304
+ });
305
+ return Promise.resolve({ok: true, status: 200, body: null, // what the redirect lands on
306
+ url: '/dashboard/?message=OTA%20binary%20uploaded&lang=EN'});
307
+ };
308
+ return Promise.resolve(fn(asked))
309
+ .finally(() => { globalThis.fetch = real.fetch; globalThis.Request = real.Request; });
310
+ }
311
+ // Opening the card reads the OTA list, so the stub has to be in place before it is built
312
+ function otaCardAdmin() {
313
+ const back = document.createElement('mqtt-projectback');
314
+ back.setAttribute('organization', 'dev');
315
+ document.body.append(back);
316
+ back.state.elements.ota.querySelector('.fi-admincard__head').click();
317
+ return {back, otaAdmin: back.state.elements.ota.querySelector('mqtt-admin')};
318
+ }
319
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
320
+
321
+ test('stays on the card it was posted from', async () => {
322
+ withCapabilities('OTAUPDATE');
323
+ await withFetch(async (asked) => {
324
+ const {back, otaAdmin} = otaCardAdmin();
325
+ const form = otaAdmin.shadowRoot.querySelector('form');
326
+ const notPrevented = form.dispatchEvent(new Event('submit', {cancelable: true, bubbles: true}));
327
+ assert.equal(notPrevented, false, 'the browser would have navigated away');
328
+ await settle();
329
+ const upload = asked.find((a) => a.url === '/ota_update');
330
+ assert.ok(upload, 'nothing was posted');
331
+ assert.equal(upload.method, 'POST');
332
+ assert.ok(back.querySelector('mqtt-admin'), 'the card should still be here');
333
+ back.remove();
334
+ });
335
+ });
336
+
337
+ test('says what the server said, and re-reads the list', async () => {
338
+ withCapabilities('OTAUPDATE');
339
+ await withFetch(async (asked) => {
340
+ const {back, otaAdmin} = otaCardAdmin();
341
+ await settle(); // the list the card reads on opening
342
+ const before = asked.filter((a) => a.url === '/ota_list/dev').length;
343
+ otaAdmin.shadowRoot.querySelector('form')
344
+ .dispatchEvent(new Event('submit', {cancelable: true, bubbles: true}));
345
+ await settle();
346
+ assert.equal(otaAdmin.state.elements.message.textContent, 'OTA binary uploaded');
347
+ assert.equal(asked.filter((a) => a.url === '/ota_list/dev').length, before + 1,
348
+ 'the list should have been re-read, which is what actually shows the upload arrived');
349
+ back.remove();
350
+ });
351
+ });
352
+ });
353
+
236
354
  describe('the page', () => {
237
355
  test('with no project chosen it says so rather than showing an empty grid', () => {
238
356
  const page = document.createElement('mqtt-dashboard');
package/test/setup.js CHANGED
@@ -25,9 +25,10 @@ for (const k of Object.getOwnPropertyNames(dom.window)) {
25
25
  }
26
26
  globalThis.window = dom.window;
27
27
  globalThis.document = dom.window.document;
28
- // Node provides its own Event/CustomEvent/EventTarget globals, which jsdom's document rejects.
29
- // The loop above skips them because they already exist, so override explicitly.
30
- for (const k of ['Event', 'CustomEvent', 'EventTarget', 'MessageEvent']) {
28
+ // Node provides its own Event/CustomEvent/EventTarget globals, which jsdom's document rejects, and
29
+ // its own FormData, which refuses to be built from a jsdom <form>. The loop above skips them
30
+ // because they already exist, so override explicitly.
31
+ for (const k of ['Event', 'CustomEvent', 'EventTarget', 'MessageEvent', 'FormData']) {
31
32
  globalThis[k] = dom.window[k];
32
33
  }
33
34