Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
>
@for (source of pathSources(); track source) {
<mat-option [value]="source">
{{source}}
{{source === 'default' ? 'Any' : source}}
</mat-option>
}
</mat-select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ import { UntypedFormControl } from '@angular/forms';
import { DatasetChartOptionsComponent } from './dataset-chart-options.component';
import { DataService } from '../../core/services/data.service';
import { UnitsService } from '../../core/services/units.service';
import { ISkPathData } from '../../core/interfaces/app-interfaces';

const src = (...keys: string[]): ISkPathData['sources'] =>
Object.fromEntries(keys.map(k => [k, { sourceTimestamp: '', sourceValue: 0 }]));

describe('DatasetChartOptionsComponent', () => {
let component: DatasetChartOptionsComponent;
let fixture: ComponentFixture<DatasetChartOptionsComponent>;
let pathObject: Partial<ISkPathData> | null;

beforeEach(async () => {
pathObject = null;
await TestBed.configureTestingModule({
imports: [DatasetChartOptionsComponent],
providers: [
{
provide: DataService,
useValue: {
getPathsAndMetaByType: () => [],
getPathObject: () => null,
getPathObject: () => pathObject,
},
},
{
Expand Down Expand Up @@ -45,4 +51,36 @@ describe('DatasetChartOptionsComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

const setPathSources = (obj: Pick<ISkPathData, 'sources'>) =>
(component as unknown as { setPathSources: (p: unknown) => void }).setPathSources(obj);
const changePath = (path: string) =>
(component as unknown as { changePath: (e: unknown) => void }).changePath({ option: { value: path } });
const sources = () => (component as unknown as { pathSources: () => string[] }).pathSources();

it('offers "Any" (default) plus the concrete source for a single-source path', () => {
setPathSources({ sources: src('gps.0') });
expect(sources()).toEqual(['default', 'gps.0']);
expect(component.datachartSource().value).toBe('default');
});

it('keeps "Any" (default) at the top and preserves the selection with multiple sources', () => {
component.datachartSource().setValue('gps.1');
setPathSources({ sources: src('gps.1', 'gps.0') });
expect(sources()).toEqual(['default', 'gps.0', 'gps.1']);
expect(component.datachartSource().value).toBe('gps.1');
});

it('defaults to "Any" (default) when no source was selected', () => {
component.datachartSource().setValue('');
setPathSources({ sources: src('gps.1', 'gps.0') });
expect(component.datachartSource().value).toBe('default');
});

it('resets a stale source to "Any" when switching to a path that lacks it', () => {
component.datachartSource().setValue('gps.9');
pathObject = { path: 'navigation.speedThroughWater', sources: src('gps.0', 'gps.1') };
changePath(pathObject.path as string);
expect(component.datachartSource().value).toBe('default');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -134,24 +134,24 @@ export class DatasetChartOptionsComponent implements OnInit {
this.setPathUnits();
return;
}
// A freshly chosen path must not carry over the previous path's concrete
// source, which may not exist here. Clear it so setPathSources falls back
// to "Any"; the ngOnInit load path calls setPathSources directly and keeps
// the saved selection.
this.datachartSource().reset();
this.setPathSources(pathObject);
this.setPathUnits(pathObject.path);
}

private setPathSources(pathObject: ISkPathData): void {
if (Object.keys(pathObject.sources).length == 1) {
this.pathSources.set(['default']);
// 'default' (shown as "Any") always leads the list: it reads the server's
// merged, priority-selected value and follows source failover. Concrete
// sources follow. An existing selection is preserved; otherwise default to "Any".
this.pathSources.set(['default', ...Object.keys(pathObject.sources).sort()]);
if (!this.datachartSource().value) {
this.datachartSource().setValue('default');
this.datachartSource().enable();
} else if (Object.keys(pathObject.sources).length > 1) {
this.pathSources.set(Object.keys(pathObject.sources).sort());
if(this.datachartSource().value) {
this.datachartSource().setValue(this.datachartSource().value);
} else {
this.datachartSource().reset();
}
this.datachartSource().enable();
}
this.datachartSource().enable();
}

private setPathUnits(path?: string): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
required>
@for (source of availableSources; track $index) {
<mat-option [value]="source">
{{source}}
{{source === 'default' ? 'Any' : source}}
</mat-option>
}
</mat-select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@ import { beforeEach, describe, expect, it } from 'vitest';
import { EMPTY } from 'rxjs';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { IDynamicControl } from '../../core/interfaces/widgets-interface';
import { ISkPathData } from '../../core/interfaces/app-interfaces';

import { PathControlConfigComponent } from './path-control-config.component';
import { SignalKConnectionService } from '../../core/services/signalk-connection.service';
import { DataService } from '../../core/services/data.service';
import { UnitsService } from '../../core/services/units.service';

const src = (...keys: string[]): ISkPathData['sources'] =>
Object.fromEntries(keys.map(k => [k, { sourceTimestamp: '', sourceValue: 0 }]));

describe('PathControlConfigComponent', () => {
let component: PathControlConfigComponent;
let fixture: ComponentFixture<PathControlConfigComponent>;
let pathForm: UntypedFormGroup;
let pathObject: Partial<ISkPathData>;

beforeEach(async () => {
pathObject = { sources: src('gps.0') };
await TestBed.configureTestingModule({
imports: [PathControlConfigComponent],
providers: [
{ provide: SignalKConnectionService, useValue: { skServerVersion: '2.14.0', serverServiceEndpoint$: EMPTY, serverVersion$: EMPTY } },
{
provide: DataService,
useValue: {
getPathObject: () => ({ displayName: 'Speed', meta: { units: 'knots' } }),
getPathObject: () => pathObject,
getPathsAndMetaByType: () => ([])
}
},
{ provide: UnitsService, useValue: { skBaseUnits: [], getConversions: () => [] } }
{ provide: UnitsService, useValue: { skBaseUnits: [], getConversions: () => [], getConversionsForPath: () => ({ base: '', conversions: [] }) } }
]
})
.compileComponents();
Expand All @@ -35,7 +42,7 @@ describe('PathControlConfigComponent', () => {
fixture = TestBed.createComponent(PathControlConfigComponent);
component = fixture.componentInstance;
// Provide required inputs before first detectChanges
const pathForm = new UntypedFormGroup({
pathForm = new UntypedFormGroup({
description: new UntypedFormControl('Speed'),
path: new UntypedFormControl('navigation.speedThroughWater'),
pathID: new UntypedFormControl('uuid-1'),
Expand All @@ -58,4 +65,45 @@ describe('PathControlConfigComponent', () => {
it('should be created', () => {
expect(component).toBeTruthy();
});

const enableFormFields = (setValues: boolean) =>
(component as unknown as { enableFormFields: (v: boolean) => void }).enableFormFields(setValues);

it('offers "Any" (default) as the only leading option for a single-source path', () => {
pathObject.sources = src('gps.0');
pathForm.controls['source'].setValue('default');
enableFormFields(false);
expect(component.availableSources).toEqual(['default', 'gps.0']);
expect(pathForm.controls['source'].value).toBe('default');
});

it('keeps "Any" (default) available when a path gains a second source', () => {
pathObject.sources = src('gps.0', 'gps.1');
pathForm.controls['source'].setValue('default');
enableFormFields(false);
expect(component.availableSources).toEqual(['default', 'gps.0', 'gps.1']);
// Regression: a saved "Any" selection must not be reset when sources multiply.
expect(pathForm.controls['source'].value).toBe('default');
});

it('preserves a concrete saved source on load', () => {
pathObject.sources = src('gps.0', 'gps.1');
pathForm.controls['source'].setValue('gps.1');
enableFormFields(false);
expect(pathForm.controls['source'].value).toBe('gps.1');
});

it('defaults an empty saved source to "Any" on load', () => {
pathObject.sources = src('gps.0', 'gps.1');
pathForm.controls['source'].setValue('');
enableFormFields(false);
expect(pathForm.controls['source'].value).toBe('default');
});

it('defaults a freshly selected path to "Any" (default)', () => {
pathObject.sources = src('gps.0', 'gps.1');
pathForm.controls['source'].setValue('gps.1');
enableFormFields(true);
expect(pathForm.controls['source'].value).toBe('default');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -216,23 +216,16 @@ export class PathControlConfigComponent implements OnInit, OnChanges {
pathFormGroup.controls['convertUnitTo'].enable({onlySelf: false});
}

if (Object.keys(pathObject.sources).length == 1) {
this.availableSources = ['default'];
if (setValues) {
if (pathFormGroup.controls['source'].value != 'default') {
pathFormGroup.controls['source'].setValue('default', {onlySelf: true});
}
}
else if (pathFormGroup.controls['source'].value != 'default') {
pathFormGroup.controls['source'].setValue('', {onlySelf: true});
}
} else if (Object.keys(pathObject.sources).length > 1) {
this.availableSources = Object.keys(pathObject.sources);
if (pathFormGroup.controls['source'].value == 'default') {
pathFormGroup.controls['source'].reset();
}
// 'default' (shown as "Any") always leads the list: it reads the server's
// merged, priority-selected value and follows source failover. Concrete
// sources follow, letting the user pin to one. A fresh path defaults to
// "Any"; an existing selection is preserved.
this.availableSources = ['default', ...Object.keys(pathObject.sources).sort()];
const sourceControl = pathFormGroup.controls['source'];
if (setValues || !sourceControl.value) {
sourceControl.setValue('default', {onlySelf: true});
}
pathFormGroup.controls['source'].enable({onlySelf: false});
sourceControl.enable({onlySelf: false});
} else {
// we don't know this path. Maybe and old saved path...
this.disablePathFields();
Expand Down
Loading