Files
iptvnator/apps/web/src/app/settings/settings.component.search.spec.ts
T
f0e51d2806 perf(web): keep lazy-only services and SafePipe out of main.js (#1729)
* perf(web): keep lazy-only services and SafePipe out of main.js

The eager shell imported barrels that re-export Angular injectables and a
pipe it never uses, and their static definitions keep those modules in
main.js: PlaylistFileImportService came with PlaylistContextFacade,
normalizeDateLocale with SafePipe, and the workspace-shell-util barrel with
SettingsContextService, which #1714 grew with match counts. That growth put
master 108 bytes over the renderer.initialBytes baseline #1712 had measured
on a branch without #1714.

Add file-level entries for the three modules and use them from the eager
and settings code: renderer.initialBytes 1,626,127 -> 1,619,993 bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(performance): lower the initial-bytes baseline to 1,619,993 bytes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:21:40 +02:00

182 lines
6.4 KiB
TypeScript

import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Router } from '@angular/router';
import { SettingsContextService } from '@iptvnator/workspace/shell/util/settings-context';
import {
SETTINGS_SEARCH_ENTRIES,
SettingsSearchEntry,
SettingsSearchService,
} from '@iptvnator/workspace/shell/util/settings-search';
import { SettingsComponent } from './settings.component';
import { SETTINGS_REVEALED_CLASS } from './settings-search.facade';
import {
configureSettingsComponentTestBed,
createElectronStub,
createEpgBridgeStub,
MockRouter,
setSettingsSearchQuery,
setSettingsSection,
stubSettingsSideEffects,
} from './test-stubs/settings-test-harness.stub';
function entry(id: string): SettingsSearchEntry {
const found = SETTINGS_SEARCH_ENTRIES.find((item) => item.id === id);
if (!found) {
throw new Error(`unknown settings entry ${id}`);
}
return found;
}
/**
* Settings search on the page: the shell writes the header term to `q`,
* the page swaps the section for ranked results, and a chosen result (or a
* command palette pick) scrolls to and highlights the row. The labels are
* translation keys here (no translations are loaded), which the index
* searches like any other text.
*/
describe('SettingsComponent search', () => {
let fixture: ComponentFixture<SettingsComponent>;
let component: SettingsComponent;
let settingsSearch: SettingsSearchService;
let settingsCtx: SettingsContextService;
let scrollIntoView: jest.Mock;
const originalElectron = window.electron;
const originalScrollIntoView = Element.prototype.scrollIntoView;
const element = (): HTMLElement => fixture.nativeElement as HTMLElement;
const query = (selector: string) => element().querySelector(selector);
beforeEach(waitForAsync(() => {
configureSettingsComponentTestBed(createEpgBridgeStub());
}));
beforeEach(async () => {
window.electron = createElectronStub();
scrollIntoView = jest.fn();
Element.prototype.scrollIntoView = scrollIntoView;
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
settingsSearch = TestBed.inject(SettingsSearchService);
settingsCtx = TestBed.inject(SettingsContextService);
stubSettingsSideEffects(component);
const loadSettings = jest.spyOn(component.form, 'loadSettings');
fixture.detectChanges();
await loadSettings.mock.results[0].value;
await fixture.whenStable();
fixture.detectChanges();
});
afterEach(() => {
jest.useRealTimers();
window.electron = originalElectron;
Element.prototype.scrollIntoView = originalScrollIntoView;
});
it('replaces the section page with ranked results while searching', () => {
expect(query('app-settings-general-section')).not.toBeNull();
setSettingsSearchQuery('theme');
fixture.detectChanges();
expect(query('app-settings-general-section')).toBeNull();
expect(
query('[data-test-id="settings-search-result-theme"]')
).not.toBeNull();
expect(
query('[data-test-id="settings-search-summary"]')?.getAttribute(
'aria-live'
)
).toBe('polite');
setSettingsSearchQuery('');
fixture.detectChanges();
expect(query('[data-test-id="settings-search-results"]')).toBeNull();
expect(query('app-settings-general-section')).not.toBeNull();
});
it('shows an empty state when nothing matches', () => {
setSettingsSearchQuery('zzzz-no-such-setting');
fixture.detectChanges();
expect(query('[data-test-id="settings-search-empty"]')).not.toBeNull();
expect(query('.settings-search-result')).toBeNull();
});
it('publishes match counts per section for the navigation', () => {
setSettingsSearchQuery('dark');
fixture.detectChanges();
expect(settingsCtx.matchCounts()).toEqual(
expect.objectContaining({ general: 1 })
);
setSettingsSearchQuery('');
fixture.detectChanges();
expect(settingsCtx.matchCounts()).toBeNull();
});
it('reveals the chosen result through the settings search service', () => {
const reveal = jest.spyOn(settingsSearch, 'reveal');
setSettingsSearchQuery('theme');
fixture.detectChanges();
(
query(
'[data-test-id="settings-search-result-theme"]'
) as HTMLButtonElement
).click();
expect(reveal).toHaveBeenCalledWith(entry('theme'));
expect(
(TestBed.inject(Router) as unknown as MockRouter).navigate
).toHaveBeenCalledWith(['/workspace/settings', 'general']);
});
it('scrolls to, focuses and briefly highlights a revealed row', () => {
jest.useFakeTimers();
settingsSearch.reveal(entry('show-captions'));
fixture.detectChanges();
const row = query('[data-setting-id="show-captions"]') as HTMLElement;
expect(scrollIntoView).toHaveBeenCalled();
expect(scrollIntoView.mock.contexts[0]).toBe(row);
expect(row.classList).toContain(SETTINGS_REVEALED_CLASS);
expect(document.activeElement).toBe(row);
expect(settingsSearch.pendingReveal()).toBeNull();
jest.runOnlyPendingTimers();
expect(row.classList).not.toContain(SETTINGS_REVEALED_CLASS);
});
it('waits for the section page before revealing a row on it', () => {
settingsSearch.reveal(entry('epg-offset'));
fixture.detectChanges();
expect(scrollIntoView).not.toHaveBeenCalled();
expect(settingsSearch.pendingReveal()?.id).toBe('epg-offset');
setSettingsSection('epg');
fixture.detectChanges();
expect(query('[data-setting-id="epg-offset"]')?.classList).toContain(
SETTINGS_REVEALED_CLASS
);
});
it('falls back to the controlling row when the searched row is hidden', () => {
// The API key row only renders while TMDB is enabled.
setSettingsSection('tmdb');
fixture.detectChanges();
expect(query('[data-setting-id="tmdb-api-key"]')).toBeNull();
settingsSearch.reveal(entry('tmdb-api-key'));
fixture.detectChanges();
expect(query('[data-setting-id="tmdb-enable"]')?.classList).toContain(
SETTINGS_REVEALED_CLASS
);
});
});