mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-28 03:51:08 -04:00
BE+FE: Plugins -> Sources DevDetails tab
This commit is contained in:
1 parent
2e2462c280
commit
f37f2c7df7
58 files changed
+2008
-200
No files matched your search
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Tests for the Sources tab's Field View backend (server/db/plugin_field_views.py).
|
||||
|
||||
Four things are tested:
|
||||
1. get_plugin_columns_for_field() correctness against a fabricated plugin list -
|
||||
every column role, both exclusion reasons, and that matching is on
|
||||
mapped_to_column, not the advisory type tag.
|
||||
2. The MAC-identity invariant holds for every real, participating plugin.
|
||||
3. The IP-discovery structural check holds against the real plugin inventory,
|
||||
and dockerfdisc/wificanary are excluded for their documented reasons.
|
||||
4. get_all_device_field_views()'s identity-based cache.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "server"))
|
||||
|
||||
from db.plugin_field_views import ( # noqa: E402
|
||||
DEVICE_FIELD_VIEWS,
|
||||
build_field_views_payload,
|
||||
get_all_device_field_views,
|
||||
get_plugin_columns_for_field,
|
||||
)
|
||||
from utils.plugin_utils import get_plugins_configs # noqa: E402
|
||||
|
||||
|
||||
def _plugin(prefix, primary_mapped_to="scanMac", extra_columns=None):
|
||||
"""Build a fake CurrentScan-mapped plugin dict for get_plugin_columns_for_field() tests."""
|
||||
columns = [{"column": "objectPrimaryId", "mapped_to_column": primary_mapped_to}]
|
||||
if extra_columns:
|
||||
columns.extend(extra_columns)
|
||||
return {
|
||||
"unique_prefix": prefix,
|
||||
"mapped_to_table": "CurrentScan",
|
||||
"database_column_definitions": columns,
|
||||
}
|
||||
|
||||
|
||||
class TestGetPluginColumnsForFieldCorrectness:
|
||||
def test_matches_objectSecondaryId(self):
|
||||
plugins = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP", "type": "device_ip"},
|
||||
])]
|
||||
assert get_plugin_columns_for_field(plugins, "ip") == [("A", "objectSecondaryId")]
|
||||
|
||||
def test_matches_watchedValue_role_regardless_of_type_tag(self):
|
||||
"""Matching is on mapped_to_column, not the advisory type tag - a mislabeled
|
||||
column (type: 'label' on an actual scanLastIP mapping) must still be included."""
|
||||
plugins = [_plugin("B", extra_columns=[
|
||||
{"column": "watchedValue2", "mapped_to_column": "scanLastIP", "type": "label"},
|
||||
])]
|
||||
assert get_plugin_columns_for_field(plugins, "ip") == [("B", "watchedValue2")]
|
||||
|
||||
def test_excludes_extra_mapped_column(self):
|
||||
"""scanLastIP mapped to 'extra' (not an identity/watched column) must be excluded -
|
||||
there's no column role get_plugin_columns_for_field() can point at."""
|
||||
plugins = [_plugin("C", extra_columns=[
|
||||
{"column": "extra", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
assert get_plugin_columns_for_field(plugins, "ip") == []
|
||||
|
||||
def test_excludes_non_scanMac_objectPrimaryId(self):
|
||||
"""A plugin whose objectPrimaryId maps to scanParentMAC (not scanMac) is excluded,
|
||||
even if it maps scanLastIP correctly - the MAC-identity invariant is checked first."""
|
||||
plugins = [_plugin("D", primary_mapped_to="scanParentMAC", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
assert get_plugin_columns_for_field(plugins, "ip") == []
|
||||
|
||||
def test_excludes_non_CurrentScan_mapped_plugin(self):
|
||||
plugin = _plugin("E", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])
|
||||
plugin["mapped_to_table"] = "Plugins_Objects"
|
||||
assert get_plugin_columns_for_field([plugin], "ip") == []
|
||||
|
||||
def test_multiple_plugins_mixed(self):
|
||||
plugins = [
|
||||
_plugin("A", extra_columns=[{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"}]),
|
||||
_plugin("B", primary_mapped_to="scanParentMAC", extra_columns=[
|
||||
{"column": "extra", "mapped_to_column": "scanLastIP"},
|
||||
]),
|
||||
_plugin("F", extra_columns=[{"column": "watchedValue1", "mapped_to_column": "scanLastIP"}]),
|
||||
]
|
||||
assert get_plugin_columns_for_field(plugins, "ip") == [
|
||||
("A", "objectSecondaryId"),
|
||||
("F", "watchedValue1"),
|
||||
]
|
||||
|
||||
|
||||
class TestRealPluginInventory:
|
||||
"""Checked against the real plugin inventory (get_plugins_configs(True)), not a
|
||||
fabricated list - these are the two tests that would catch a real plugin's config.json
|
||||
silently breaking the invariant get_plugin_columns_for_field() enforces."""
|
||||
|
||||
def test_mac_identity_invariant_holds_for_participants(self):
|
||||
all_plugins = get_plugins_configs(True)
|
||||
participants = get_plugin_columns_for_field(all_plugins, "ip")
|
||||
by_prefix = {p.get("unique_prefix"): p for p in all_plugins}
|
||||
|
||||
for prefix, _column in participants:
|
||||
plugin = by_prefix[prefix]
|
||||
primary_id_col = next(
|
||||
c for c in plugin["database_column_definitions"]
|
||||
if c.get("column") == "objectPrimaryId"
|
||||
)
|
||||
assert primary_id_col.get("mapped_to_column") == "scanMac", (
|
||||
f"{prefix} participates in the 'ip' field view but its objectPrimaryId "
|
||||
f"does not map to scanMac - this should have been excluded"
|
||||
)
|
||||
|
||||
def test_ip_discovery_structural_check(self):
|
||||
all_plugins = get_plugins_configs(True)
|
||||
participants = get_plugin_columns_for_field(all_plugins, "ip")
|
||||
participant_prefixes = {prefix for prefix, _ in participants}
|
||||
|
||||
assert "dockerdisc" not in participant_prefixes, (
|
||||
"dockerdisc maps its IP to 'extra' and objectPrimaryId to scanParentMAC - "
|
||||
"excluded for both reasons, should never appear here"
|
||||
)
|
||||
assert "wificanary" not in participant_prefixes, (
|
||||
"wificanary has no MAC-based objectPrimaryId at all - should never appear here"
|
||||
)
|
||||
|
||||
valid_roles = {"objectPrimaryId", "objectSecondaryId", "watchedValue1", "watchedValue2", "watchedValue3", "watchedValue4"}
|
||||
for prefix, column in participants:
|
||||
assert column in valid_roles, f"{prefix} returned an invalid column role: {column}"
|
||||
|
||||
# Incidental today - not frozen. A future plugin addition legitimately changes this.
|
||||
assert len(participants) == 23
|
||||
|
||||
|
||||
class TestGetAllDeviceFieldViewsCache:
|
||||
def test_same_object_returns_cached_result(self):
|
||||
plugins = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
first = get_all_device_field_views(plugins)
|
||||
second = get_all_device_field_views(plugins)
|
||||
assert first is second
|
||||
|
||||
def test_different_object_recomputes(self):
|
||||
plugins_v1 = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
plugins_v2 = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
]), _plugin("G", extra_columns=[
|
||||
{"column": "watchedValue3", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
|
||||
result_v1 = get_all_device_field_views(plugins_v1)
|
||||
result_v2 = get_all_device_field_views(plugins_v2)
|
||||
|
||||
assert result_v1 is not result_v2
|
||||
assert result_v1["ip"] == [("A", "objectSecondaryId")]
|
||||
assert result_v2["ip"] == [("A", "objectSecondaryId"), ("G", "watchedValue3")]
|
||||
|
||||
def test_covers_every_registered_field(self):
|
||||
plugins = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
result = get_all_device_field_views(plugins)
|
||||
assert set(result.keys()) == set(DEVICE_FIELD_VIEWS.keys())
|
||||
|
||||
|
||||
class TestBuildFieldViewsPayload:
|
||||
"""The exact shape embedded into plugins.json's field_views block - the frontend
|
||||
contract, not just producer/helper equality. Each field_key maps to an object
|
||||
carrying its own label_key/value_type alongside its columns, so the frontend
|
||||
reads DEVICE_FIELD_VIEWS' declared label/rendering type instead of re-deriving
|
||||
them per field_key."""
|
||||
|
||||
def test_matches_get_all_device_field_views_content(self):
|
||||
plugins = [
|
||||
_plugin("A", extra_columns=[{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"}]),
|
||||
_plugin("F", extra_columns=[{"column": "watchedValue2", "mapped_to_column": "scanLastIP"}]),
|
||||
]
|
||||
payload = build_field_views_payload(plugins)
|
||||
expected = get_all_device_field_views(plugins)
|
||||
|
||||
assert payload["ip"]["columns"] == [
|
||||
{"plugin": prefix, "column": column} for prefix, column in expected["ip"]
|
||||
]
|
||||
|
||||
def test_entry_shape_has_label_key_value_type_and_columns(self):
|
||||
plugins = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
payload = build_field_views_payload(plugins)
|
||||
|
||||
assert set(payload["ip"].keys()) == {"label_key", "value_type", "columns"}
|
||||
assert payload["ip"]["label_key"] == DEVICE_FIELD_VIEWS["ip"]["label_key"]
|
||||
assert payload["ip"]["value_type"] == DEVICE_FIELD_VIEWS["ip"]["value_type"]
|
||||
assert len(payload["ip"]["columns"]) == 1
|
||||
column_entry = payload["ip"]["columns"][0]
|
||||
assert set(column_entry.keys()) == {"plugin", "column"}
|
||||
assert column_entry == {"plugin": "A", "column": "objectSecondaryId"}
|
||||
|
||||
def test_covers_every_registered_field_with_its_own_label_key(self):
|
||||
plugins = [_plugin("A", extra_columns=[
|
||||
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP"},
|
||||
])]
|
||||
payload = build_field_views_payload(plugins)
|
||||
|
||||
assert set(payload.keys()) == set(DEVICE_FIELD_VIEWS.keys())
|
||||
for field_key, field_def in DEVICE_FIELD_VIEWS.items():
|
||||
assert payload[field_key]["label_key"] == field_def["label_key"]
|
||||
assert payload[field_key]["value_type"] == field_def["value_type"]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Sources / Session Info characterization tests
|
||||
|
||||
Playwright specs for the Sources tab (Plugin View + Field View) and the
|
||||
Session Info tab. These are characterization tests, not unit tests of
|
||||
internal functions: each one mocks the relevant GraphQL/REST calls via
|
||||
`page.route()` with fixed fixture data (no real DB, no seeded scan data)
|
||||
and asserts on rendered, observable DOM - the same assertions should hold
|
||||
against a future frontend rewrite, unchanged.
|
||||
|
||||
Run against a live NetAlertX instance (defaults to `http://localhost:20211`,
|
||||
override with `NAX_BASE_URL`):
|
||||
|
||||
```
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
Scope is deliberately narrow - four specs covering the Sources/Session Info
|
||||
PRD's acceptance criteria, not a general frontend test suite. Do not add
|
||||
unrelated specs here without a separate, deliberate decision to broaden this
|
||||
setup.
|
||||
|
||||
**Status as of this implementation pass:** written and manually reviewed for
|
||||
correctness against the actual frontend code (GraphQL shapes, route URLs,
|
||||
DOM selectors), but not yet run against a live instance - no running
|
||||
NetAlertX server was available in the environment these were written in.
|
||||
Run them against a real devcontainer before considering this PRD's Playwright
|
||||
work complete.
|
||||
@@ -0,0 +1,32 @@
|
||||
// Test #8 (PRD): ?tab=<plugin_prefix> (existing deep-link) and ?field=<key>
|
||||
// (new) must not interfere with each other's resolution. Note: the
|
||||
// top-level Sources tab itself isn't auto-activated by either param today
|
||||
// (initializeTabs() on the device-details page only reads cache, not the
|
||||
// URL) - that's pre-existing behavior, unchanged by this PRD, so both tests
|
||||
// click the Sources tab explicitly before asserting, matching reality.
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { TEST_MAC, mockSourcesBackend } = require('./fixtures');
|
||||
|
||||
test('?tab=<plugin> deep-link resolves to Plugin View with that plugin active', async ({ page }) => {
|
||||
await mockSourcesBackend(page);
|
||||
|
||||
await page.goto(`/deviceDetails.php?mac=${encodeURIComponent(TEST_MAC)}&tab=FREEBOX`);
|
||||
await page.click('#tabSources');
|
||||
|
||||
await expect(page.locator('#sourcesPluginView')).toBeVisible();
|
||||
await expect(page.locator('#sourcesFieldView')).toBeHidden();
|
||||
await expect(page.locator('#sourcesViewToggle')).toHaveAttribute('data-view', 'plugin');
|
||||
await expect(page.locator('a#FREEBOX_id')).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('?field=<key> deep-link resolves to Field View with that field active', async ({ page }) => {
|
||||
await mockSourcesBackend(page);
|
||||
|
||||
await page.goto(`/deviceDetails.php?mac=${encodeURIComponent(TEST_MAC)}&field=ip`);
|
||||
await page.click('#tabSources');
|
||||
|
||||
await expect(page.locator('#sourcesFieldView')).toBeVisible();
|
||||
await expect(page.locator('#sourcesPluginView')).toBeHidden();
|
||||
await expect(page.locator('#sourcesViewToggle')).toHaveAttribute('data-view', 'field');
|
||||
await expect(page.locator('#fieldPane_ip')).toHaveClass(/active/);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// Test #4 (PRD): Field View's join logic - picks each plugin's value from
|
||||
// whichever Plugins_Objects column field_views.ip names for it, not a
|
||||
// hardcoded column role, and status renders as plugin-observation wording.
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { TEST_MAC, mockSourcesBackend } = require('./fixtures');
|
||||
|
||||
test('Field View renders Plugin/Value/Status/Last-Changed from the correct column per plugin', async ({ page }) => {
|
||||
await mockSourcesBackend(page);
|
||||
|
||||
await page.goto(`/deviceDetails.php?mac=${encodeURIComponent(TEST_MAC)}`);
|
||||
await page.click('#tabSources');
|
||||
await page.click('#sourcesViewToggle');
|
||||
await page.click('#sourcesViewMenu a[data-view="field"]');
|
||||
|
||||
const rows = page.locator('#fieldPane_ip tbody tr');
|
||||
await expect(rows).toHaveCount(3);
|
||||
|
||||
// FREEBOX's value comes from objectSecondaryId.
|
||||
await expect(rows.filter({ hasText: 'FREEBOX' })).toContainText('fe80::1');
|
||||
// ARPSCAN's value comes from watchedValue1 - not objectSecondaryId.
|
||||
await expect(rows.filter({ hasText: 'ARPSCAN' })).toContainText('192.168.1.50');
|
||||
// SNMPDSC's value comes from watchedValue2 - proves the join isn't
|
||||
// hardcoded to either of the other two roles.
|
||||
await expect(rows.filter({ hasText: 'SNMPDSC' })).toContainText('fe80::2');
|
||||
|
||||
// missing-in-last-scan renders as plugin-observation wording, not a
|
||||
// liveness claim.
|
||||
const snmpRow = rows.filter({ hasText: 'SNMPDSC' });
|
||||
await expect(snmpRow).toContainText('Not reported last run');
|
||||
await expect(snmpRow).not.toContainText('offline');
|
||||
await expect(snmpRow).not.toContainText('unreachable');
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
// Shared fixture data and route-mocking helpers for the Sources/Session Info
|
||||
// characterization tests. Every spec uses these instead of hand-rolling its
|
||||
// own mock shapes, so a GraphQL/REST contract change only needs updating here.
|
||||
|
||||
const TEST_MAC = 'aa:bb:cc:dd:ee:01';
|
||||
|
||||
const PLUGINS_JSON_FIXTURE = {
|
||||
data: [
|
||||
{ unique_prefix: 'FREEBOX', show_ui: true, mapped_to_table: 'CurrentScan' },
|
||||
{ unique_prefix: 'ARPSCAN', show_ui: true, mapped_to_table: 'CurrentScan' },
|
||||
{ unique_prefix: 'SNMPDSC', show_ui: true, mapped_to_table: 'CurrentScan' },
|
||||
],
|
||||
field_views: {
|
||||
ip: {
|
||||
label_key: 'DevDetail_SessionTable_IP',
|
||||
value_type: 'device_ip',
|
||||
columns: [
|
||||
{ plugin: 'FREEBOX', column: 'objectSecondaryId' },
|
||||
{ plugin: 'ARPSCAN', column: 'watchedValue1' },
|
||||
{ plugin: 'SNMPDSC', column: 'watchedValue2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const PLUGINS_OBJECTS_FIXTURE = [
|
||||
{
|
||||
plugin: 'FREEBOX', objectSecondaryId: 'fe80::1', watchedValue1: null, watchedValue2: null,
|
||||
watchedValue3: null, watchedValue4: null, status: 'watched-not-changed', dateTimeChanged: '2026-10-05 10:00:00',
|
||||
},
|
||||
{
|
||||
plugin: 'ARPSCAN', objectSecondaryId: null, watchedValue1: '192.168.1.50', watchedValue2: null,
|
||||
watchedValue3: null, watchedValue4: null, status: 'new', dateTimeChanged: '2026-10-05 10:01:00',
|
||||
},
|
||||
{
|
||||
plugin: 'SNMPDSC', objectSecondaryId: null, watchedValue1: null, watchedValue2: 'fe80::2',
|
||||
watchedValue3: null, watchedValue4: null, status: 'missing-in-last-scan', dateTimeChanged: '2026-10-05 09:50:00',
|
||||
},
|
||||
];
|
||||
|
||||
const DEVICE_DATA_FIXTURE = {
|
||||
devMac: TEST_MAC,
|
||||
devName: 'Test Device',
|
||||
devStatus: 'Online',
|
||||
devPresentLastScan: 1,
|
||||
devAlertDown: 1,
|
||||
devFlapping: 0,
|
||||
devIsSleeping: 0,
|
||||
devIsArchived: 0,
|
||||
devIsNew: 0,
|
||||
devPrimaryIPv4: '192.168.1.50',
|
||||
devPrimaryIPv6: 'fe80::1',
|
||||
devFQDN: 'test-device.lan',
|
||||
devLastConnection: '2026-10-05 10:00:00',
|
||||
devFirstConnection: '2025-01-01 00:00:00',
|
||||
};
|
||||
|
||||
// Routes every GraphQL call through to the right fixture based on the query
|
||||
// text in the POST body, and plugins.json/device endpoint by URL.
|
||||
async function mockSourcesBackend(page, { mac = TEST_MAC, pluginsObjects = PLUGINS_OBJECTS_FIXTURE } = {}) {
|
||||
let pluginsObjectsCallCount = 0;
|
||||
|
||||
await page.route('**/php/server/query_json.php?file=plugins.json*', route => {
|
||||
route.fulfill({ json: PLUGINS_JSON_FIXTURE });
|
||||
});
|
||||
|
||||
await page.route('**/server/graphql', route => {
|
||||
const body = JSON.parse(route.request().postData());
|
||||
|
||||
if (body.query.includes('pluginsObjects')) {
|
||||
pluginsObjectsCallCount += 1;
|
||||
route.fulfill({
|
||||
json: { data: { pluginsObjects: { entries: pluginsObjects } } },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other GraphQL query (settings, etc.) this flow happens to fire -
|
||||
// return an empty-but-valid shape rather than failing the request.
|
||||
route.fulfill({ json: { data: {} } });
|
||||
});
|
||||
|
||||
await page.route(`**/server/device/${encodeURIComponent(mac)}*`, route => {
|
||||
route.fulfill({ json: DEVICE_DATA_FIXTURE });
|
||||
});
|
||||
|
||||
return {
|
||||
getPluginsObjectsCallCount: () => pluginsObjectsCallCount,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TEST_MAC,
|
||||
PLUGINS_JSON_FIXTURE,
|
||||
PLUGINS_OBJECTS_FIXTURE,
|
||||
DEVICE_DATA_FIXTURE,
|
||||
mockSourcesBackend,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
// Test #5 (PRD): Session Info renders without a settings lookup - all 6
|
||||
// migrated fields display, including devStatus (the one that's never
|
||||
// actually worked before this PRD), and there's no save/submit control.
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { TEST_MAC, mockSourcesBackend } = require('./fixtures');
|
||||
|
||||
test('Session Info displays all 6 migrated fields including devStatus, with no save control', async ({ page }) => {
|
||||
await mockSourcesBackend(page);
|
||||
|
||||
await page.goto(`/deviceDetails.php?mac=${encodeURIComponent(TEST_MAC)}`);
|
||||
await page.click('#tabSessionInfo');
|
||||
|
||||
await expect(page.locator('#sessionInfo_devPrimaryIPv4')).toHaveText('192.168.1.50');
|
||||
await expect(page.locator('#sessionInfo_devPrimaryIPv6')).toHaveText('fe80::1');
|
||||
await expect(page.locator('#sessionInfo_devFQDN')).toHaveText('test-device.lan');
|
||||
await expect(page.locator('#sessionInfo_devLastConnection')).not.toBeEmpty();
|
||||
await expect(page.locator('#sessionInfo_devFirstConnection')).not.toBeEmpty();
|
||||
|
||||
// devStatus: the dead field this PRD fixes - must actually render something,
|
||||
// not be silently blank the way it always has been before this change.
|
||||
await expect(page.locator('#sessionInfo_devStatus')).not.toBeEmpty();
|
||||
|
||||
await expect(page.locator('#panSessionInfo button[type="submit"]')).toHaveCount(0);
|
||||
await expect(page.locator('#panSessionInfo .pa-btn-save, #panSessionInfo #btnSave')).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// Test #6 (PRD): Known IPs (Session Info) and Field View read the same
|
||||
// pluginsObjects data via exactly one fetch - the data assertion alone
|
||||
// wouldn't catch two independently-issued calls that happen to agree, so
|
||||
// this also asserts the call count.
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { TEST_MAC, mockSourcesBackend } = require('./fixtures');
|
||||
|
||||
test('Known IPs and Field View show consistent filtered data via one shared fetch', async ({ page }) => {
|
||||
const mocks = await mockSourcesBackend(page);
|
||||
|
||||
await page.goto(`/deviceDetails.php?mac=${encodeURIComponent(TEST_MAC)}`);
|
||||
|
||||
// Open Session Info first.
|
||||
await page.click('#tabSessionInfo');
|
||||
await expect(page.locator('#sessionInfo_knownIps')).toContainText('fe80::1');
|
||||
await expect(page.locator('#sessionInfo_knownIps')).toContainText('192.168.1.50');
|
||||
// SNMPDSC's fe80::2 is missing-in-last-scan - excluded from Known IPs.
|
||||
await expect(page.locator('#sessionInfo_knownIps')).not.toContainText('fe80::2');
|
||||
|
||||
// Then open Sources -> Field View, which shows the full, unfiltered set.
|
||||
await page.click('#tabSources');
|
||||
await page.click('#sourcesViewToggle');
|
||||
await page.click('#sourcesViewMenu a[data-view="field"]');
|
||||
const rows = page.locator('#fieldPane_ip tbody tr');
|
||||
await expect(rows).toHaveCount(3); // includes the missing-in-last-scan row
|
||||
|
||||
// The actual proof of the shared-fetch mechanism: exactly one network
|
||||
// call for pluginsObjects across both views, not one per view.
|
||||
expect(mocks.getPluginsObjectsCallCount()).toBe(1);
|
||||
});
|
||||
Reference in new issue
Block a user