/** * Location-sensor readings should cost one request per location, not two. * * Card view (SpoolLocationFooter) and the table's Temperature/Humidity/ * Battery columns used to each fetch their own copy — the footer under a * 'cardOnly' key, the table under an 'all' key — so a location with sensors * bound cost two requests per poll interval whenever a card was on screen, * and the table's 'all' request fired even with all three sensor columns * hidden, which is the default. Both queries now share one key and one * unfiltered fetch; the footer filters to show_on_card itself. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../utils'; import InventoryPageRouter from '../../pages/InventoryPage'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; const mockSpool = { id: 1, material: 'PLA', subtype: null, brand: 'Polymaker', color_name: 'Red', rgba: 'FF0000FF', label_weight: 1000, core_weight: 250, weight_used: 100, slicer_filament: null, slicer_filament_name: null, nozzle_temp_min: null, nozzle_temp_max: null, note: null, added_full: null, last_used: null, encode_time: null, tag_uid: null, tray_uuid: null, data_origin: null, tag_type: null, archived_at: null, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', k_profiles: [], cost_per_kg: null, last_scale_weight: null, last_weighed_at: null, location_id: 7, storage_location: 'Drybox 1', }; const mockSpoolAtUnsensoredLocation = { ...mockSpool, id: 2, brand: 'eSun', location_id: 8, storage_location: 'Shelf 2', }; const mockSensor = { id: 1, location_id: 7, name: 'Drybox 1 Temperature', entity_id: 'sensor.drybox_1_temperature', kind: 'numeric', device_class: 'temperature', unit: '°C', alert_state: null, alert_above: null, alert_below: null, notify_on_alert: false, show_on_card: true, sort_order: 0, last_state: '24.5', last_changed: '2025-01-01T00:00:00Z', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; const mockReading = { id: 1, name: 'Drybox 1 Temperature', entity_id: 'sensor.drybox_1_temperature', kind: 'numeric', device_class: 'temperature', unit: '°C', state: '24.5', value: 24.5, alerting: false, reachable: true, alert_state: null, alert_above: null, alert_below: null, last_changed: '2025-01-01T00:00:00Z', show_on_card: true, }; const COLUMN_CONFIG_KEY = 'bambuddy-inventory-columns'; describe('InventoryPage - location sensor readings request count', () => { let readingsRequestCount: number; beforeEach(() => { vi.mocked(window.localStorage.getItem).mockReset(); vi.mocked(window.localStorage.setItem).mockReset(); readingsRequestCount = 0; server.use( http.get('/api/v1/inventory/spools', () => HttpResponse.json([mockSpool])), http.get('/api/v1/location-ha-sensors/', () => HttpResponse.json([mockSensor])), http.get('/api/v1/location-ha-sensors/by-location/7/readings', () => { readingsRequestCount += 1; return HttpResponse.json([mockReading]); }), ); }); it('does not fetch readings in table view with sensor columns hidden (the default)', async () => { render(); await waitFor(() => { expect(screen.getAllByText('Polymaker').length).toBeGreaterThan(0); }); // Give any stray request a moment to land before asserting its absence. await new Promise((r) => setTimeout(r, 50)); expect(readingsRequestCount).toBe(0); }); it('fetches readings exactly once in table view when a sensor column is visible', async () => { vi.mocked(window.localStorage.getItem).mockImplementation((key: string) => key === COLUMN_CONFIG_KEY ? JSON.stringify([{ id: 'temperature', label: 'Temperature', visible: true }]) : null ); render(); await waitFor(() => expect(screen.getByText('24.50 °C')).toBeInTheDocument()); await new Promise((r) => setTimeout(r, 50)); expect(readingsRequestCount).toBe(1); }); it('fetches readings exactly once in card view, shared between the table query and the footer', async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(screen.getAllByText('Polymaker').length).toBeGreaterThan(0)); await user.click(screen.getByText('Cards')); await waitFor(() => expect(screen.getByText('24.50 °C')).toBeInTheDocument()); await new Promise((r) => setTimeout(r, 50)); expect(readingsRequestCount).toBe(1); }); it('hides a sensor with show_on_card=false from the card footer without a second request', async () => { server.use( http.get('/api/v1/location-ha-sensors/by-location/7/readings', () => { readingsRequestCount += 1; return HttpResponse.json([{ ...mockReading, show_on_card: false }]); }), ); const user = userEvent.setup(); render(); await waitFor(() => expect(screen.getAllByText('Polymaker').length).toBeGreaterThan(0)); await user.click(screen.getByText('Cards')); // The footer never renders — its one sensor is hidden from cards — but // that must be a client-side filter, not a second, differently-scoped // request. await new Promise((r) => setTimeout(r, 50)); expect(screen.queryByText('24.50 °C')).toBeNull(); expect(readingsRequestCount).toBe(1); }); it('never queries a location with no bound sensor, in either view', async () => { // The gate that started this file: a location absent from // getLocationHASensors() must never appear in the readings useQueries // array at all — not merely disabled — so it costs nothing whether its // card/row is on screen or not. Most installs have far more storage // locations than ones actually wired up to Home Assistant. let unsensoredLocationRequestCount = 0; vi.mocked(window.localStorage.getItem).mockImplementation((key: string) => key === COLUMN_CONFIG_KEY ? JSON.stringify([{ id: 'temperature', label: 'Temperature', visible: true }]) : null ); server.use( http.get('/api/v1/inventory/spools', () => HttpResponse.json([mockSpool, mockSpoolAtUnsensoredLocation]) ), http.get('/api/v1/location-ha-sensors/by-location/8/readings', () => { unsensoredLocationRequestCount += 1; return HttpResponse.json([]); }), ); const user = userEvent.setup(); render(); // Table view, temperature column visible: location 7 (has a sensor) is queried. await waitFor(() => expect(screen.getByText('24.50 °C')).toBeInTheDocument()); expect(screen.getAllByText('eSun').length).toBeGreaterThan(0); await new Promise((r) => setTimeout(r, 50)); expect(readingsRequestCount).toBe(1); expect(unsensoredLocationRequestCount).toBe(0); // Card view too — the footer for location 8's card must not query it either. await user.click(screen.getByText('Cards')); await waitFor(() => expect(screen.getAllByText('eSun').length).toBeGreaterThan(0)); await new Promise((r) => setTimeout(r, 50)); expect(unsensoredLocationRequestCount).toBe(0); }); });