file manager: filter as a collapsed checkbox dropdown
Replace the <select multiple size=4> filter with a single-row button that
expands into a scrollable checkbox menu (All + Custom files, then the
Identifiers/PLMN/Services headings with the categories and per-service
options), mirroring simtrace-analyser's command filter.
- pysimFsFilterSummary/Label, pysimFsFilterToggle, pysimFsFilterSetEntry
(all-exclusive, empty -> all) and a rewritten pysimFsFilterRender;
pysimFsSetFilter is gone.
- Closes on outside click and Escape. i18n: "Selected: {n}".
- No CSS rebuild needed (swapped to utility classes already in the
prebuilt style.css).
Frontend 1146 tests green. (Version/sw bump still deferred.)
This commit is contained in:
1 parent
01e50d841f
commit
d1067e3e72
2 files changed
+257
-104
No files matched your search
+89
-36
@@ -1119,7 +1119,10 @@
|
||||
<span class="pysim-fs-sort-pill px-2 py-0.5 rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300" data-fs-sort="name" onclick="pysimFsSetSort('name')" data-l10n="Name">Name</span>
|
||||
<span class="mx-1 text-gray-400 dark:text-slate-600">·</span>
|
||||
<span data-l10n="Filter:">Filter:</span>
|
||||
<select id="pysim-fs-filter" multiple size="4" onchange="pysimFsSetFilter()" class="text-xs bg-white dark:bg-slate-800 border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 text-gray-700 dark:text-slate-200 max-w-48"></select>
|
||||
<div class="relative">
|
||||
<button type="button" id="pysim-fs-filter-btn" aria-haspopup="true" aria-expanded="false" onclick="pysimFsFilterToggle()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 max-w-xs truncate">All</button>
|
||||
<div id="pysim-fs-filter-menu" role="group" aria-label="Filter" class="hidden absolute left-0 mt-1 z-50 w-64 max-h-64 overflow-y-auto rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 p-2 shadow-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
|
||||
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
|
||||
@@ -16155,45 +16158,82 @@ function pysimFsFilterRestore() {
|
||||
}
|
||||
pysimFsFilterRestore();
|
||||
|
||||
function pysimFsFilterRender() {
|
||||
const el = document.getElementById('pysim-fs-filter');
|
||||
if (!el) return;
|
||||
const services = fsFilterServiceOptions();
|
||||
if (!FS_FILTER_GROUPS.length && !services.length) {
|
||||
el.innerHTML = '<option disabled>' + esc(t('(no filters yet)')) + '</option>';
|
||||
return;
|
||||
}
|
||||
const opt = o => '<option value="' + esc(o.id) + '">' + esc(t(o.label)) + '</option>';
|
||||
let out = '<option value="' + esc(FS_FILTER_ALL) + '">' + esc(t('All')) + '</option>' +
|
||||
'<option value="' + esc(FS_FILTER_CUSTOM) + '">' + esc(t('Custom files')) + '</option>';
|
||||
for (const group of FS_FILTER_GROUP_ORDER) {
|
||||
const items = FS_FILTER_GROUPS.filter(g => g.group === group).map(opt);
|
||||
if (group === 'Services') items.push(...services.map(opt));
|
||||
if (items.length) out += '<optgroup label="' + esc(t(group)) + '">' + items.join('') + '</optgroup>';
|
||||
}
|
||||
el.innerHTML = out;
|
||||
el.querySelectorAll('option').forEach(o => { o.selected = pysimFsFilter.has(o.value); });
|
||||
// The label of one filter entry: the custom flag, a category's own name or
|
||||
// the feature name of a per-service entry.
|
||||
function pysimFsFilterLabel(id) {
|
||||
if (id === FS_FILTER_CUSTOM) return t('Custom files');
|
||||
const category = FS_FILTER_GROUPS.find(g => g.id === id);
|
||||
if (category) return t(category.label);
|
||||
if (id.indexOf('svc:') === 0) return id.slice(4);
|
||||
return id;
|
||||
}
|
||||
|
||||
function pysimFsSetFilter() {
|
||||
const el = document.getElementById('pysim-fs-filter');
|
||||
if (!el) return;
|
||||
const prev = pysimFsFilter;
|
||||
const next = new Set(Array.from(el.selectedOptions).map(o => o.value).filter(v => v));
|
||||
// `all` is exclusive: choosing it clears the category selections, choosing
|
||||
// a category clears it. A native multiselect's onchange reports only the
|
||||
// new selection, so the previous one tells the two gestures apart: `all`
|
||||
// just appearing means it was clicked (it clears the rest); it staying
|
||||
// next to a new category means the category was clicked (it drops `all`).
|
||||
if (next.has(FS_FILTER_ALL)) {
|
||||
if (!prev.has(FS_FILTER_ALL)) {
|
||||
next.clear();
|
||||
next.add(FS_FILTER_ALL);
|
||||
} else {
|
||||
next.delete(FS_FILTER_ALL);
|
||||
// The row's caption: the default or a single choice by name, otherwise the
|
||||
// number of selected entries.
|
||||
function pysimFsFilterSummary() {
|
||||
if (!pysimFsFilter.size || pysimFsFilter.has(FS_FILTER_ALL)) return t('All');
|
||||
if (pysimFsFilter.size === 1) return pysimFsFilterLabel(Array.from(pysimFsFilter)[0]);
|
||||
return t('Selected: {n}').replace('{n}', pysimFsFilter.size);
|
||||
}
|
||||
|
||||
// The collapsed filter: a single row that expands into a checkbox list -
|
||||
// "All"/"Custom files", then a heading per group with its categories (the
|
||||
// Services heading carries one checkbox per file-backed feature).
|
||||
function pysimFsFilterRender() {
|
||||
const menu = document.getElementById('pysim-fs-filter-menu');
|
||||
const btn = document.getElementById('pysim-fs-filter-btn');
|
||||
if (!menu) return;
|
||||
const services = fsFilterServiceOptions();
|
||||
const item = (id, label) =>
|
||||
'<label class="flex items-center gap-2 px-2 py-0.5 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 text-xs text-gray-700 dark:text-slate-200">' +
|
||||
'<input type="checkbox" data-filter="' + esc(id) + '"' +
|
||||
(pysimFsFilter.has(id) ? ' checked' : '') +
|
||||
' onchange="pysimFsFilterSetEntry(this.dataset.filter, this.checked)">' +
|
||||
'<span>' + esc(label) + '</span></label>';
|
||||
const heading = group =>
|
||||
'<div class="px-2 mt-1 uppercase text-gray-500 dark:text-slate-400">' + esc(t(group)) + '</div>';
|
||||
if (!FS_FILTER_GROUPS.length && !services.length) {
|
||||
menu.innerHTML = '<div class="px-2 text-gray-500 dark:text-slate-400">' + esc(t('(no filters yet)')) + '</div>';
|
||||
} else {
|
||||
let out = item(FS_FILTER_ALL, t('All')) + item(FS_FILTER_CUSTOM, t('Custom files'));
|
||||
for (const group of FS_FILTER_GROUP_ORDER) {
|
||||
const categories = FS_FILTER_GROUPS.filter(g => g.group === group);
|
||||
const extra = group === 'Services' ? services : [];
|
||||
if (!categories.length && !extra.length) continue;
|
||||
out += heading(group);
|
||||
out += categories.map(g => item(g.id, t(g.label))).join('');
|
||||
out += extra.map(o => item(o.id, o.label)).join('');
|
||||
}
|
||||
menu.innerHTML = out;
|
||||
}
|
||||
if (btn) btn.textContent = pysimFsFilterSummary();
|
||||
}
|
||||
|
||||
// Expand or collapse the menu (`force` overrides the toggle; the outside
|
||||
// click and Escape handlers pass `false`).
|
||||
function pysimFsFilterToggle(force) {
|
||||
const menu = document.getElementById('pysim-fs-filter-menu');
|
||||
const btn = document.getElementById('pysim-fs-filter-btn');
|
||||
if (!menu) return;
|
||||
const open = force === undefined ? menu.classList.contains('hidden') : !!force;
|
||||
if (open) pysimFsFilterRender();
|
||||
menu.classList.toggle('hidden', !open);
|
||||
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
}
|
||||
|
||||
// One checkbox: `all` is exclusive, and an emptied selection means "all".
|
||||
function pysimFsFilterSetEntry(entry, on) {
|
||||
const next = new Set(pysimFsFilter);
|
||||
if (entry === FS_FILTER_ALL) {
|
||||
next.clear();
|
||||
next.add(FS_FILTER_ALL);
|
||||
} else if (on) {
|
||||
next.delete(FS_FILTER_ALL);
|
||||
next.add(entry);
|
||||
} else {
|
||||
next.delete(entry);
|
||||
if (!next.size) next.add(FS_FILTER_ALL);
|
||||
}
|
||||
if (!next.size) next.add(FS_FILTER_ALL);
|
||||
pysimFsFilter = next;
|
||||
localStorage.setItem('simple_fs_filter', JSON.stringify(Array.from(pysimFsFilter)));
|
||||
pysimFsFilterRender();
|
||||
@@ -27875,6 +27915,7 @@ const LANG_RU = {
|
||||
'SMS-related': 'Связанное с SMS',
|
||||
'CB-related': 'Связанное с CB',
|
||||
'Custom files': 'Пользовательские файлы',
|
||||
'Selected: {n}': 'Выбрано: {n}',
|
||||
'Probe all files': 'Проверить все файлы',
|
||||
'Feature': 'Функция',
|
||||
'SST': 'SST',
|
||||
@@ -28984,6 +29025,18 @@ updateConvFields();
|
||||
updateSp();
|
||||
uiccStdFilesLoad();
|
||||
pysimFsFilterRender();
|
||||
// the file-tree filter's dropdown closes on an outside click or Escape (the
|
||||
// button toggles it, so a click on the button itself is left to its handler)
|
||||
document.addEventListener('click', e => {
|
||||
const target = e.target;
|
||||
if (!target || !target.closest) return;
|
||||
if (!target.closest('#pysim-fs-filter-btn') && !target.closest('#pysim-fs-filter-menu')) {
|
||||
pysimFsFilterToggle(false);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') pysimFsFilterToggle(false);
|
||||
});
|
||||
updateSpKic();
|
||||
updateSpKid();
|
||||
chainInit('chain-sim');
|
||||
|
||||
@@ -59,13 +59,48 @@ globalThis.localStorage = {
|
||||
globalThis.esc = s => String(s);
|
||||
globalThis.t = k => k;
|
||||
globalThis.pysimFsRenderTree = () => { _treeRenders++; };
|
||||
let _select = null;
|
||||
globalThis.document = { getElementById: id => (id === 'pysim-fs-filter' ? _select : null) };
|
||||
|
||||
// The filter's two DOM elements: the collapsed row's button and the hidden
|
||||
// menu it expands into. The menu only needs a settable innerHTML (the
|
||||
// render never reads the DOM back) and a classList for the open/closed state.
|
||||
function fakeBtn() {
|
||||
return {
|
||||
textContent: '',
|
||||
attrs: {},
|
||||
setAttribute(k, v) { this.attrs[k] = String(v); },
|
||||
getAttribute(k) { return this.attrs[k]; },
|
||||
};
|
||||
}
|
||||
|
||||
function fakeMenu() {
|
||||
const classes = new Set(['hidden']);
|
||||
return {
|
||||
_html: '',
|
||||
classes,
|
||||
classList: {
|
||||
contains: c => classes.has(c),
|
||||
toggle(c, force) {
|
||||
const on = force === undefined ? !classes.has(c) : !!force;
|
||||
if (on) classes.add(c); else classes.delete(c);
|
||||
return on;
|
||||
},
|
||||
},
|
||||
get innerHTML() { return this._html; },
|
||||
set innerHTML(v) { this._html = v; },
|
||||
};
|
||||
}
|
||||
|
||||
const dom = { menu: null, btn: null };
|
||||
globalThis.document = {
|
||||
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
|
||||
: (id === 'pysim-fs-filter-btn' ? dom.btn : null)),
|
||||
};
|
||||
|
||||
let code = '';
|
||||
for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex',
|
||||
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterRender', 'pysimFsSetFilter',
|
||||
'pysimFsFilterRestore']) {
|
||||
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
|
||||
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
|
||||
'pysimFsFilterSetEntry', 'pysimFsFilterRestore']) {
|
||||
code += extractFunc(html, f) + '\n';
|
||||
}
|
||||
for (const c of ['FS_FILTER_GROUPS', 'FS_FILTER_GROUP_ORDER', 'SERVICES_FS_MAP']) code += extractConst(html, c);
|
||||
@@ -79,27 +114,39 @@ function withFilter(ids, fn) {
|
||||
try { return fn(); } finally { pysimFsFilter = saved; }
|
||||
}
|
||||
|
||||
function fakeSelect(selected) {
|
||||
const options = (selected || []).map(v => ({ value: v, selected: true }));
|
||||
return {
|
||||
_html: '',
|
||||
get innerHTML() { return this._html; },
|
||||
set innerHTML(v) {
|
||||
this._html = v;
|
||||
options.length = 0;
|
||||
for (const m of v.matchAll(/<option value="([^"]*)"[^>]*>/g)) options.push({ value: m[1], selected: false });
|
||||
},
|
||||
querySelectorAll() { return options; },
|
||||
get selectedOptions() { return options.filter(o => o.selected); },
|
||||
options,
|
||||
};
|
||||
// The checkbox rows the render wrote: [{id, checked}] in document order.
|
||||
function renderedBoxes() {
|
||||
const out = [];
|
||||
for (const m of (dom.menu ? dom.menu.innerHTML : '').matchAll(
|
||||
/<input type="checkbox" data-filter="([^"]*)"( checked)?/g)) {
|
||||
out.push({ id: m[1], checked: !!m[2] });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const box = id => renderedBoxes().find(b => b.id === id);
|
||||
|
||||
const fileBackedFeatures = () => Object.keys(SERVICES_FS_MAP).filter(n => SERVICES_FS_MAP[n].length);
|
||||
|
||||
test('the file tree has a multiselect filter next to the sort pills', () => {
|
||||
assert.match(html, /id="pysim-fs-filter"[^>]*multiple[^>]*onchange="pysimFsSetFilter\(\)"/);
|
||||
test('the file tree has a collapsed filter dropdown next to the sort pills', () => {
|
||||
assert.match(html, /id="pysim-fs-filter-btn"[^>]*aria-haspopup="true"[^>]*aria-expanded="false"[^>]*onclick="pysimFsFilterToggle\(\)"/);
|
||||
assert.match(html, /id="pysim-fs-filter-menu"[^>]*role="group"[^>]*class="hidden absolute/);
|
||||
assert.match(html, /data-l10n="Filter:"/);
|
||||
// the multiselect (and its onchange handler) is gone
|
||||
assert.ok(!/id="pysim-fs-filter"/.test(html));
|
||||
assert.ok(!/pysimFsSetFilter\(/.test(html));
|
||||
});
|
||||
|
||||
test('an outside click or Escape closes the menu', () => {
|
||||
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
|
||||
assert.ok(click, 'no document click listener');
|
||||
assert.match(click[1], /#pysim-fs-filter-btn/);
|
||||
assert.match(click[1], /#pysim-fs-filter-menu/);
|
||||
assert.match(click[1], /pysimFsFilterToggle\(false\)/);
|
||||
const key = html.match(/document\.addEventListener\('keydown', e => \{([\s\S]{0,300}?)\n\}\);/);
|
||||
assert.ok(key, 'no document keydown listener');
|
||||
assert.match(key[1], /Escape/);
|
||||
assert.match(key[1], /pysimFsFilterToggle\(false\)/);
|
||||
});
|
||||
|
||||
test('the six category groups name their files', () => {
|
||||
@@ -235,32 +282,110 @@ test('a directory stays while it has a matching descendant', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('selecting "all" clears the categories', () => {
|
||||
pysimFsFilter = new Set(['identifiers']);
|
||||
_select = fakeSelect(['all', 'identifiers']);
|
||||
test('the menu lists all, custom, the three headings and the services', () => {
|
||||
pysimFsFilter = new Set(['all']);
|
||||
dom.menu = fakeMenu();
|
||||
dom.btn = fakeBtn();
|
||||
pysimFsFilterRender();
|
||||
const out = dom.menu.innerHTML;
|
||||
assert.match(out, /^<label[^>]*><input type="checkbox" data-filter="all" checked/);
|
||||
assert.match(out, /data-filter="custom"/);
|
||||
assert.match(out, /<div class="[^"]*">Identifiers<\/div>/);
|
||||
assert.match(out, /<div class="[^"]*">PLMN<\/div>/);
|
||||
assert.match(out, /<div class="[^"]*">Services<\/div>/);
|
||||
// the headings come in the declared order
|
||||
assert.ok(out.indexOf('>Identifiers</div>') < out.indexOf('>PLMN</div>'));
|
||||
assert.ok(out.indexOf('>PLMN</div>') < out.indexOf('>Services</div>'));
|
||||
// the categories sit under their headings
|
||||
assert.ok(out.indexOf('data-filter="provider-names"') < out.indexOf('>PLMN</div>'));
|
||||
assert.ok(out.indexOf('data-filter="plmn-selection"') > out.indexOf('>PLMN</div>'));
|
||||
assert.ok(out.indexOf('data-filter="sms"') > out.indexOf('>Services</div>'));
|
||||
const boxes = renderedBoxes();
|
||||
assert.strictEqual(boxes.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length);
|
||||
assert.strictEqual(boxes[0].id, 'all');
|
||||
assert.strictEqual(boxes[0].checked, true);
|
||||
assert.strictEqual(box('custom').checked, false);
|
||||
assert.ok(boxes.some(b => b.id === 'svc:Service Provider Name (SPN)'));
|
||||
assert.ok(!boxes.some(b => b.id === 'svc:Proactive SIM'));
|
||||
// the row shows the summary, and the checkbox calls the setter directly
|
||||
assert.strictEqual(dom.btn.textContent, 'All');
|
||||
assert.match(out, /onchange="pysimFsFilterSetEntry\(this\.dataset\.filter, this\.checked\)"/);
|
||||
});
|
||||
|
||||
test('the menu marks the saved selection', () => {
|
||||
pysimFsFilter = new Set(['svc:Short Message Storage (SMS)', 'identifiers']);
|
||||
dom.menu = fakeMenu();
|
||||
dom.btn = fakeBtn();
|
||||
pysimFsFilterRender();
|
||||
assert.deepStrictEqual(renderedBoxes().filter(b => b.checked).map(b => b.id),
|
||||
['identifiers', 'svc:Short Message Storage (SMS)']);
|
||||
assert.strictEqual(box('all').checked, false);
|
||||
assert.strictEqual(dom.btn.textContent, 'Selected: 2');
|
||||
});
|
||||
|
||||
test('the button summary names the single entry or counts the selection', () => {
|
||||
const summary = ids => withFilter(ids, () => pysimFsFilterSummary());
|
||||
assert.strictEqual(summary(['all']), 'All');
|
||||
// an empty selection also means everything
|
||||
assert.strictEqual(summary([]), 'All');
|
||||
assert.strictEqual(summary(['all', 'sms']), 'All');
|
||||
assert.strictEqual(summary(['custom']), 'Custom files');
|
||||
assert.strictEqual(summary(['plmn-selection']), 'PLMN selection');
|
||||
assert.strictEqual(summary(['svc:Service Provider Name (SPN)']), 'Service Provider Name (SPN)');
|
||||
assert.strictEqual(summary(['sms', 'cb']), 'Selected: 2');
|
||||
// an unknown id falls back to itself
|
||||
assert.strictEqual(summary(['providers']), 'providers');
|
||||
});
|
||||
|
||||
test('the menu toggles open and closed', () => {
|
||||
dom.menu = fakeMenu();
|
||||
dom.btn = fakeBtn();
|
||||
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
|
||||
pysimFsFilterToggle();
|
||||
assert.strictEqual(dom.menu.classList.contains('hidden'), false);
|
||||
assert.strictEqual(dom.btn.getAttribute('aria-expanded'), 'true');
|
||||
pysimFsFilterToggle();
|
||||
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
|
||||
assert.strictEqual(dom.btn.getAttribute('aria-expanded'), 'false');
|
||||
// the forced forms (the outside click / Escape handlers pass false)
|
||||
pysimFsFilterToggle(true);
|
||||
assert.strictEqual(dom.menu.classList.contains('hidden'), false);
|
||||
pysimFsFilterToggle(false);
|
||||
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
|
||||
});
|
||||
|
||||
test('the checkbox rows keep "all" exclusive and persist the selection', () => {
|
||||
dom.menu = fakeMenu();
|
||||
dom.btn = fakeBtn();
|
||||
delete _store.simple_fs_filter;
|
||||
_treeRenders = 0;
|
||||
pysimFsSetFilter();
|
||||
pysimFsFilter = new Set(['all']);
|
||||
pysimFsFilterSetEntry('all', true);
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||
assert.strictEqual(_store.simple_fs_filter, '["all"]');
|
||||
assert.strictEqual(_treeRenders, 1);
|
||||
});
|
||||
|
||||
test('selecting a category drops "all"', () => {
|
||||
pysimFsFilter = new Set(['all']);
|
||||
_select = fakeSelect(['all', 'sms']);
|
||||
pysimFsSetFilter();
|
||||
// choosing a category drops "all"
|
||||
pysimFsFilterSetEntry('sms', true);
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['sms']);
|
||||
assert.strictEqual(_store.simple_fs_filter, '["sms"]');
|
||||
});
|
||||
|
||||
test('several categories stay selected and an empty selection means all', () => {
|
||||
pysimFsFilter = new Set(['all']);
|
||||
_select = fakeSelect(['identifiers', 'sms']);
|
||||
pysimFsSetFilter();
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['identifiers', 'sms']);
|
||||
pysimFsFilter = new Set(['identifiers', 'sms']);
|
||||
_select = fakeSelect([]);
|
||||
pysimFsSetFilter();
|
||||
assert.strictEqual(_treeRenders, 2);
|
||||
assert.strictEqual(box('sms').checked, true);
|
||||
assert.strictEqual(box('all').checked, false);
|
||||
assert.strictEqual(dom.btn.textContent, 'SMS-related');
|
||||
// several categories stay selected
|
||||
pysimFsFilterSetEntry('identifiers', true);
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['sms', 'identifiers']);
|
||||
// unchecking one keeps the other
|
||||
pysimFsFilterSetEntry('sms', false);
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['identifiers']);
|
||||
// unchecking the last one restores the default
|
||||
pysimFsFilterSetEntry('identifiers', false);
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||
assert.strictEqual(_store.simple_fs_filter, '["all"]');
|
||||
assert.strictEqual(box('all').checked, true);
|
||||
// "all" is exclusive even when another entry is selected
|
||||
pysimFsFilter = new Set(['sms']);
|
||||
pysimFsFilterSetEntry('all', true);
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||
});
|
||||
|
||||
@@ -283,41 +408,16 @@ test('the saved selection is restored, defaulting to "all"', () => {
|
||||
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||
});
|
||||
|
||||
test('the dropdown renders all, the groups and the services', () => {
|
||||
pysimFsFilter = new Set(['all']);
|
||||
const el = fakeSelect(['all']);
|
||||
_select = el;
|
||||
pysimFsFilterRender();
|
||||
assert.match(el.innerHTML, /^<option value="all">All<\/option><option value="custom">Custom files<\/option>/);
|
||||
assert.match(el.innerHTML, /<optgroup label="Identifiers"><option value="identifiers">Identifiers<\/option><option value="provider-names">Provider names<\/option><\/optgroup>/);
|
||||
assert.match(el.innerHTML, /<optgroup label="PLMN"><option value="plmn-selection">PLMN selection<\/option><option value="wlan">WLAN \/ I-WLAN<\/option><\/optgroup>/);
|
||||
assert.match(el.innerHTML, /<optgroup label="Services"><option value="sms">SMS-related<\/option><option value="cb">CB-related<\/option><option value="svc:Local Phone Book \(ADN\)">/);
|
||||
const values = [...el.innerHTML.matchAll(/<option value="([^"]*)"/g)].map(m => m[1]);
|
||||
assert.strictEqual(values.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length);
|
||||
assert.ok(values.includes('svc:Service Provider Name (SPN)'));
|
||||
assert.ok(values.includes('custom'));
|
||||
assert.ok(!values.includes('svc:Proactive SIM'));
|
||||
assert.strictEqual(el.options.find(o => o.value === 'all').selected, true);
|
||||
});
|
||||
|
||||
test('the dropdown marks the saved selection', () => {
|
||||
pysimFsFilter = new Set(['svc:Short Message Storage (SMS)']);
|
||||
const el = fakeSelect();
|
||||
_select = el;
|
||||
pysimFsFilterRender();
|
||||
const selected = el.options.filter(o => o.selected).map(o => o.value);
|
||||
assert.deepStrictEqual(selected, ['svc:Short Message Storage (SMS)']);
|
||||
});
|
||||
|
||||
test('the render consults the filter when walking children', () => {
|
||||
assert.match(html, /if \(!pysimFsFilterShow\(child\)\) continue;/);
|
||||
assert.ok(!/pysimFsFilterMatch\(child\)/.test(html));
|
||||
});
|
||||
|
||||
test('the filter state is persisted under simple_fs_filter', () => {
|
||||
const fn = extractFunc(html, 'pysimFsSetFilter');
|
||||
const fn = extractFunc(html, 'pysimFsFilterSetEntry');
|
||||
assert.ok(fn.indexOf("localStorage.setItem('simple_fs_filter'") > 0, fn);
|
||||
assert.ok(fn.indexOf('pysimFsRenderTree()') > 0, fn);
|
||||
assert.ok(fn.indexOf('pysimFsFilterRender()') > 0, fn);
|
||||
});
|
||||
|
||||
test('the dropdown is re-rendered on language switch', () => {
|
||||
@@ -328,7 +428,7 @@ test('the dropdown is re-rendered on language switch', () => {
|
||||
test('the filter labels have Russian entries', () => {
|
||||
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
|
||||
for (const key of ['All', 'Custom files', 'Identifiers', 'PLMN', 'Services', 'PLMN selection',
|
||||
'Provider names', 'WLAN / I-WLAN', 'SMS-related', 'CB-related']) {
|
||||
'Provider names', 'WLAN / I-WLAN', 'SMS-related', 'CB-related', 'Selected: {n}']) {
|
||||
assert.ok(dict.includes("'" + key + "':"), 'missing Russian entry: ' + key);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user