App-Netdisco
view release on metacpan or search on metacpan
xt/js/netmap-loading.test.js view on Meta::CPAN
// The netmap pane must say something while it waits for its first paint.
//
// netdisco.js's do_search shows "Waiting for results..." only while it fetches
// /ajax/content/device/netmap. That fragment is about 500 bytes and arrives in
// a few milliseconds, and the moment it does, $(target).html(content) replaces
// the message. The wait the user actually experiences is the one after that:
// the fragment's own $.getJSON to /ajax/data/device/netmap, then parsing the
// payload, then the first force layout. Measured against a 6481 node network
// that is 4.8MB over about a second, plus layout, with nothing on screen.
//
// So the fragment carries its own indicator, and the data callback removes it.
// The wrap also needs a height of its own: until ForceGraph sizes the
// container, the wrap's only in-flow child is empty, so it collapses to zero
// height and the absolutely positioned spinner and fullscreen control resolve
// against a box with no room, landing above the pane instead of inside it.
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const repoRoot = path.join(__dirname, '..', '..');
const read = (...parts) => fs.readFileSync(path.join(repoRoot, ...parts), 'utf8');
const LOADING_ID = 'nd2_netmap-loading';
describe('netmap first paint indicator', () => {
test('fragment__before_the_data_arrives__ships_a_loading_indicator', () => {
const fragment = read('share', 'views', 'ajax', 'device', 'netmap.tt');
assert.match(
fragment,
new RegExp(`id="${LOADING_ID}"`),
`share/views/ajax/device/netmap.tt must carry an element with id "${LOADING_ID}", ` +
'or the pane is blank for the whole data request and first layout'
);
assert.match(
fragment,
/fa-spinner fa-spin/,
'the indicator must use the same spinner glyph as netdisco.js\'s "Waiting for results..." ' +
'so the netmap pane does not announce a slow load differently from every other tab'
);
});
test('dataCallback__once_the_graph_is_built__removes_the_loading_indicator', () => {
const js = read('share', 'views', 'js', 'netmap.js');
assert.match(
js,
new RegExp(LOADING_ID),
`share/views/js/netmap.js must reference "${LOADING_ID}"`
);
assert.match(
js,
new RegExp(`${LOADING_ID}[\\s\\S]{0,200}?\\.remove\\(\\)`),
`share/views/js/netmap.js must remove "${LOADING_ID}", or it stays on top of the map`
);
});
// Re-submitting the netmap sidebar form replaces the pane while the previous
// instance is still running, and nothing destroys it until the next
// fragment's data callback, so its handlers fire against a spinner that has
// left the document.
test('spinnerHandlers__pane_replaced_mid_run__do_not_dereference_a_missing_element', () => {
const js = read('share', 'views', 'js', 'netmap.js');
assert.doesNotMatch(
js,
/getElementById\(['"]nd2_netmap-spinner['"]\)\s*\./,
'share/views/js/netmap.js must not read a property straight off the spinner lookup: ' +
'onEngineTick and onEngineStop both outlive the element on a reload in place, ' +
'and an unguarded read throws a TypeError into the console on every re-submit'
);
});
test('wrap__before_the_canvas_exists__has_a_height_of_its_own', () => {
const css = read('share', 'public', 'css', 'netdisco.css');
const rule = css.match(/#nd2_netmap-wrap\s*\{[^}]*\}/);
assert.ok(rule, 'netdisco.css must carry a #nd2_netmap-wrap rule');
assert.match(
rule[0],
/min-height/,
'without a min-height the wrap collapses until ForceGraph sizes the container, ' +
'so the spinner and fullscreen control render above the pane rather than in it'
);
});
});
( run in 0.685 second using v1.01-cache-2.11-cpan-b16cb0d3907 )