Skip to content

Commit 70cd3fa

Browse files
committed
test: add tests
1 parent 2bab3dc commit 70cd3fa

8 files changed

Lines changed: 691 additions & 28 deletions

File tree

packages/compas-open-scd/src/compas-services/CompasCodeComponentsService.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,34 @@ export interface CodeComponentEntry {
1010

1111
export type EditionComponents = Partial<Record<string, CodeComponentEntry>>;
1212

13-
interface CodeComponentsJson {
13+
export interface CodeComponentsJson {
1414
IEC61850_code_components: Partial<Record<IEC61850Edition, EditionComponents>>;
1515
}
1616

1717
const CODE_COMPONENTS_PATH = 'public/xml/IEC61850_code_components.json';
1818

19-
let cachedComponents: CodeComponentsJson | null = null;
19+
let cachedRequest: Promise<CodeComponentsJson | null> | null = null;
2020

2121
/**
22-
* Fetches and caches the IEC61850_code_components.json file.
23-
*
24-
* The file is served from `public/xml/`. At deploy time the private repo
25-
* overwrites the bundled baseline with the latest version — so this is always
26-
* a single fetch against one location.
27-
*
28-
* Dispatches an error log event on the given element if the file cannot be
29-
* fetched or parsed.
22+
* Fetches and caches the IEC61850_code_components.json file from `public/xml/`.
3023
*
3124
* @param element - Element used for dispatching error log events.
3225
* @returns The parsed JSON, or null on failure.
3326
*/
3427
export async function loadCodeComponentsJson(
3528
element: Element
3629
): Promise<CodeComponentsJson | null> {
37-
if (cachedComponents !== null) {
38-
return cachedComponents;
39-
}
30+
cachedRequest ??= fetchCodeComponentsJson(element);
31+
return cachedRequest;
32+
}
4033

34+
export function resetCodeComponentsCache(): void {
35+
cachedRequest = null;
36+
}
37+
38+
async function fetchCodeComponentsJson(
39+
element: Element
40+
): Promise<CodeComponentsJson | null> {
4141
try {
4242
const response = await fetch(CODE_COMPONENTS_PATH);
4343
if (!response.ok) {
@@ -46,14 +46,14 @@ export async function loadCodeComponentsJson(
4646
CODE_COMPONENTS_PATH,
4747
response.status
4848
);
49+
cachedRequest = null;
4950
return null;
5051
}
51-
const json = (await response.json()) as CodeComponentsJson;
52-
cachedComponents = json;
53-
return json;
52+
return (await response.json()) as CodeComponentsJson;
5453
} catch (err) {
5554
console.warn('Failed to fetch IEC61850 code components JSON:', err);
5655
dispatchCodeComponentsError(element, CODE_COMPONENTS_PATH);
56+
cachedRequest = null;
5757
return null;
5858
}
5959
}

packages/compas-open-scd/src/compas/CompasNsdoc.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,10 @@ export async function loadNsdocFilesForEdition(
4040
return;
4141
}
4242

43-
const parts: Array<[string, string]> = [
44-
['7-2', 'IEC 61850-7-2'],
45-
['7-3', 'IEC 61850-7-3'],
46-
['7-4', 'IEC 61850-7-4'],
47-
['8-1', 'IEC 61850-8-1'],
48-
];
43+
const parts = ['7-2', '7-3', '7-4', '8-1'];
4944

5045
await Promise.all(
51-
parts.map(async ([part, nsdocId]) => {
46+
parts.map(async (part) => {
5247
const entry: CodeComponentEntry | undefined = editionComponents[part];
5348
if (!entry?.NSDOC) {
5449
component.dispatchEvent(

packages/compas-open-scd/src/open-scd.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
html,
44
LitElement,
55
property,
6-
query,
76
state,
87
TemplateResult,
98
} from 'lit-element';

packages/compas-open-scd/src/translations/de.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ export const de: Translations = {
3636
fileMissing: 'NSDoc-Datei nicht verfügbar',
3737
fileMissingDetails:
3838
'NSDoc-Datei konnte nicht geladen werden: {{filename}}',
39-
editionNotSupported: 'IEC 61850 Edition noch nicht unterstützt',
39+
editionNotSupported:
40+
'NSDoc-Dateien für IEC 61850 Edition {{edition}} konnten nicht geladen werden',
4041
editionNotSupportedDetails:
41-
'Keine Code-Komponenten für Edition {{edition}} definiert. Es wird auf Mustererkennung zurückgegriffen.',
42+
'Für Edition {{edition}} sind keine Code-Komponenten konfiguriert. NSDoc-basierte Validierung ist nicht verfügbar.',
4243
},
4344
changeset: {
4445
major: '???',

packages/compas-open-scd/src/translations/en.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ export const en = {
3333
nsdoc: {
3434
fileMissing: 'NSDoc file not available',
3535
fileMissingDetails: 'Could not load NSDoc file: {{filename}}',
36-
editionNotSupported: 'IEC 61850 edition not yet supported',
36+
editionNotSupported:
37+
'Could not load NSDoc files for IEC 61850 edition {{edition}}',
3738
editionNotSupportedDetails:
38-
'No code components defined for edition {{edition}}. Falling back to pattern discovery.',
39+
'No code components are configured for edition {{edition}}. NSDoc-based validation will not be available.',
3940
},
4041
changeset: {
4142
major: 'Major change',
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { expect } from '@open-wc/testing';
2+
import sinon, { SinonStub } from 'sinon';
3+
4+
import {
5+
loadCodeComponentsJson,
6+
resetCodeComponentsCache,
7+
getEditionComponents,
8+
} from '../../../src/compas-services/CompasCodeComponentsService.js';
9+
import type {
10+
CodeComponentsJson,
11+
IEC61850Edition,
12+
} from '../../../src/compas-services/CompasCodeComponentsService.js';
13+
14+
const VALID_JSON: CodeComponentsJson = {
15+
IEC61850_code_components: {
16+
Edition_2: {
17+
'7-2': { NSD: 'IEC_61850-7-2_Ed2.nsd', NSDOC: 'IEC_61850-7-2_Ed2.nsdoc' },
18+
'7-3': { NSD: 'IEC_61850-7-3_Ed2.nsd', NSDOC: 'IEC_61850-7-3_Ed2.nsdoc' },
19+
},
20+
Edition_2_1: {
21+
'7-2': {
22+
NSD: 'IEC_61850-7-2_Ed2.1.nsd',
23+
NSDOC: 'IEC_61850-7-2_Ed2.1.nsdoc',
24+
},
25+
},
26+
},
27+
};
28+
29+
describe('loadCodeComponentsJson', () => {
30+
let component: Element;
31+
let fetchStub: SinonStub;
32+
33+
beforeEach(() => {
34+
component = document.createElement('div');
35+
fetchStub = sinon.stub(globalThis, 'fetch');
36+
resetCodeComponentsCache();
37+
});
38+
39+
afterEach(() => {
40+
sinon.restore();
41+
resetCodeComponentsCache();
42+
});
43+
44+
describe('on success', () => {
45+
beforeEach(() => {
46+
fetchStub.callsFake(() =>
47+
Promise.resolve(
48+
new Response(JSON.stringify(VALID_JSON), { status: 200 })
49+
)
50+
);
51+
});
52+
53+
it('returns the parsed JSON', async () => {
54+
const result = await loadCodeComponentsJson(component);
55+
56+
expect(result).to.deep.equal(VALID_JSON);
57+
});
58+
59+
it('does not dispatch any log events', async () => {
60+
const logEvents: CustomEvent[] = [];
61+
component.addEventListener('log', e => logEvents.push(e as CustomEvent));
62+
63+
await loadCodeComponentsJson(component);
64+
65+
expect(logEvents).to.have.length(0);
66+
});
67+
68+
it('fetches from the correct path', async () => {
69+
await loadCodeComponentsJson(component);
70+
71+
expect(fetchStub).to.have.been.calledWith(
72+
'public/xml/IEC61850_code_components.json'
73+
);
74+
});
75+
76+
it('only fetches once for concurrent calls', async () => {
77+
await Promise.all([
78+
loadCodeComponentsJson(component),
79+
loadCodeComponentsJson(component),
80+
loadCodeComponentsJson(component),
81+
]);
82+
83+
expect(fetchStub).to.have.been.calledOnce;
84+
});
85+
86+
it('only fetches once for sequential calls', async () => {
87+
await loadCodeComponentsJson(component);
88+
await loadCodeComponentsJson(component);
89+
90+
expect(fetchStub).to.have.been.calledOnce;
91+
});
92+
});
93+
94+
describe('when fetch returns a non-ok response', () => {
95+
beforeEach(() => {
96+
fetchStub.callsFake(() =>
97+
Promise.resolve(new Response(null, { status: 404 }))
98+
);
99+
});
100+
101+
it('returns null', async () => {
102+
const result = await loadCodeComponentsJson(component);
103+
104+
expect(result).to.be.null;
105+
});
106+
107+
it('dispatches an error log event', async () => {
108+
const logEvents: CustomEvent[] = [];
109+
component.addEventListener('log', e => logEvents.push(e as CustomEvent));
110+
111+
await loadCodeComponentsJson(component);
112+
113+
expect(logEvents).to.have.length(1);
114+
expect(logEvents[0].detail.kind).to.equal('error');
115+
});
116+
117+
it('clears the cache so the next call retries', async () => {
118+
await loadCodeComponentsJson(component);
119+
fetchStub.callsFake(() =>
120+
Promise.resolve(
121+
new Response(JSON.stringify(VALID_JSON), { status: 200 })
122+
)
123+
);
124+
125+
const result = await loadCodeComponentsJson(component);
126+
127+
expect(result).to.deep.equal(VALID_JSON);
128+
expect(fetchStub).to.have.been.calledTwice;
129+
});
130+
});
131+
132+
describe('when fetch throws a network error', () => {
133+
beforeEach(() => {
134+
fetchStub.callsFake(() => Promise.reject(new TypeError('Network error')));
135+
});
136+
137+
it('returns null', async () => {
138+
const result = await loadCodeComponentsJson(component);
139+
140+
expect(result).to.be.null;
141+
});
142+
143+
it('dispatches an error log event', async () => {
144+
const logEvents: CustomEvent[] = [];
145+
component.addEventListener('log', e => logEvents.push(e as CustomEvent));
146+
147+
await loadCodeComponentsJson(component);
148+
149+
expect(logEvents).to.have.length(1);
150+
expect(logEvents[0].detail.kind).to.equal('error');
151+
});
152+
153+
it('clears the cache so the next call retries', async () => {
154+
await loadCodeComponentsJson(component);
155+
fetchStub.callsFake(() =>
156+
Promise.resolve(
157+
new Response(JSON.stringify(VALID_JSON), { status: 200 })
158+
)
159+
);
160+
161+
const result = await loadCodeComponentsJson(component);
162+
163+
expect(result).to.deep.equal(VALID_JSON);
164+
expect(fetchStub).to.have.been.calledTwice;
165+
});
166+
});
167+
});
168+
169+
describe('getEditionComponents', () => {
170+
it('returns the components for an existing edition', () => {
171+
const result = getEditionComponents(VALID_JSON, 'Edition_2');
172+
173+
expect(result).to.deep.equal(
174+
VALID_JSON.IEC61850_code_components['Edition_2']
175+
);
176+
});
177+
178+
it('returns null when the edition is not present', () => {
179+
const result = getEditionComponents(VALID_JSON, 'Edition_1');
180+
181+
expect(result).to.be.null;
182+
});
183+
184+
it('returns null when the edition entry is an empty object', () => {
185+
const json: CodeComponentsJson = {
186+
IEC61850_code_components: { Edition_2: {} },
187+
};
188+
189+
const result = getEditionComponents(json, 'Edition_2');
190+
191+
expect(result).to.be.null;
192+
});
193+
194+
it('returns the components for each valid edition', () => {
195+
const editions: IEC61850Edition[] = ['Edition_2', 'Edition_2_1'];
196+
197+
for (const edition of editions) {
198+
const result = getEditionComponents(VALID_JSON, edition);
199+
expect(result).to.not.be.null;
200+
}
201+
});
202+
});

0 commit comments

Comments
 (0)