diff --git a/packages/compas-open-scd/public/nsdoc/README.md b/packages/compas-open-scd/public/nsdoc/README.md index 8df94c234e..1ae14a7237 100644 --- a/packages/compas-open-scd/public/nsdoc/README.md +++ b/packages/compas-open-scd/public/nsdoc/README.md @@ -1,6 +1,17 @@ Directory containing fixed nsdoc for the 'Validation' plugin: -- IEC_61850-7-2_2007B3-en.nsdoc -- IEC_61850-7-3_2007B3-en.nsdoc -- IEC_61850-7-4_2007B3-en.nsdoc -- IEC_61850-8-1_2003A2-en.nsdoc +- IEC_61850-7-2_2007XX-en.nsdoc +- IEC_61850-7-3_2007XX-en.nsdoc +- IEC_61850-7-4_2007XX-en.nsdoc +- IEC_61850-8-1_2003XX-en.nsdoc + +Where `XX` represents the revision and release values. + +This directory should also contain a `manifest.json` file listing all available NSDOC files for dynamic discovery. If the manifest is not present, the system will fall back to pattern-based file discovery. + +## Fallback pattern discovery + +When manifest.json is not available, the service checks for files using these patterns: + +- Standards 7-2, 7-3, 7-4: `2007B5` to `2007B9` (stops at first found) +- Standard 8-1: `2003A2` to `2003A9` (stops at first found) diff --git a/packages/compas-open-scd/public/nsdoc/manifest.json.example b/packages/compas-open-scd/public/nsdoc/manifest.json.example new file mode 100644 index 0000000000..52a354ef48 --- /dev/null +++ b/packages/compas-open-scd/public/nsdoc/manifest.json.example @@ -0,0 +1,8 @@ + +[ + "IEC_61850-7-2_2007B5-en.nsdoc", + "IEC_61850-7-3_2007B5-en.nsdoc", + "IEC_61850-7-4_2007B5-en.nsdoc", + "IEC_61850-8-1_2003A2-en.nsdoc" +] + diff --git a/packages/compas-open-scd/public/xml/IEC_61850-7-2_2007B3.nsd b/packages/compas-open-scd/public/xml/IEC_61850-7-2_2007B5.nsd similarity index 98% rename from packages/compas-open-scd/public/xml/IEC_61850-7-2_2007B3.nsd rename to packages/compas-open-scd/public/xml/IEC_61850-7-2_2007B5.nsd index 9635bf57ce..ba98cb1ef4 100644 --- a/packages/compas-open-scd/public/xml/IEC_61850-7-2_2007B3.nsd +++ b/packages/compas-open-scd/public/xml/IEC_61850-7-2_2007B5.nsd @@ -5,10 +5,15 @@ id="IEC 61850-7-2" version="2007" revision="B" - release="3" - umlVersion="WG10built4" - umlDate="2019-10-02T00:00:00Z" - publicationStage="IS"> + release="5" + umlVersion="WG10built12" + umlDate="2024-01-15" + publicationStage="IS" + appVersion="j61850DocBuilder 02.03 based on jCleanCim noNS beta9.3 (derived from jCleanCim 02-02)" + namespaceType="basic" + nsdVersion="2017" + nsdRevision="B" + nsdRelease="5"> COPYRIGHT (c) IEC, www.iec.ch/tc57/supportdocuments. This version of this NSD is part of IEC_61850-7-2:2010 Edition 2.1; see the IEC_61850-7-2:2010 Edition 2.1 for full legal notices. In case of any differences between the here-below code and the IEC published content, the here-below definition supersedes the IEC publication; it may contain updates. See history files. The whole document has to be taken into account to have a full description of this code component. @@ -17,8 +22,8 @@ + revision="B" + tissues="1781, 1782, 1801, 1841, 1847, 1822"/> @@ -531,4 +536,4 @@ presCond="M"/> - \ No newline at end of file + diff --git a/packages/compas-open-scd/public/xml/IEC_61850-7-3_2007B3.nsd b/packages/compas-open-scd/public/xml/IEC_61850-7-3_2007B5.nsd similarity index 99% rename from packages/compas-open-scd/public/xml/IEC_61850-7-3_2007B3.nsd rename to packages/compas-open-scd/public/xml/IEC_61850-7-3_2007B5.nsd index 777a13cf59..7ec83fc09f 100644 --- a/packages/compas-open-scd/public/xml/IEC_61850-7-3_2007B3.nsd +++ b/packages/compas-open-scd/public/xml/IEC_61850-7-3_2007B5.nsd @@ -5,10 +5,15 @@ id="IEC 61850-7-3" version="2007" revision="B" - release="3" - umlVersion="WG10built3" - umlDate="2019-10-02T00:00:00Z" - publicationStage="IS"> + release="5" + umlVersion="WG10built12" + umlDate="2024-02-12" + publicationStage="IS" + appVersion="j61850DocBuilder 02.03 based on jCleanCim noNS beta9.3 (derived from jCleanCim 02-02)" + namespaceType="basic" + nsdVersion="2017" + nsdRevision="B" + nsdRelease="5"> COPYRIGHT (c) IEC, www.iec.ch/tc57/supportdocuments. This version of this NSD is part of IEC_61850-7-3:2010 Edition 2.1; see the IEC_61850-7-3:2010 Edition 2.1 for full legal notices. In case of any differences between the here-below code and the IEC published content, the here-below definition supersedes the IEC publication; it may contain updates. See history files. The whole document has to be taken into account to have a full description of this code component. @@ -17,9 +22,12 @@ - + revision="B" + tissues="1716, 1730, 1783, 1785, 1807, 1829, 1840, 1851, 1852, 1889, 1900"/> + @@ -232,7 +240,7 @@ descID="IEC61850_7_3.DAEnums::MultiplierKind.n.desc"/> + descID="IEC61850_7_3.DAEnums::MultiplierKind.µ.desc"/> @@ -385,7 +393,7 @@ descID="IEC61850_7_3.DAEnums::SIUnitKind.Bq.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.°C.desc"/> @@ -436,31 +444,31 @@ descID="IEC61850_7_3.DAEnums::SIUnitKind.Pa.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.m².desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.m³.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.m_per_s².desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.m³_per_s.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.m_per_m³.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.kg_per_m³.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.m²_per_s.desc"/> @@ -478,10 +486,10 @@ descID="IEC61850_7_3.DAEnums::SIUnitKind.rad_per_s.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.W_per_m².desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.J_per_m².desc"/> @@ -516,16 +524,16 @@ descID="IEC61850_7_3.DAEnums::SIUnitKind.Vs.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.V².desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.A².desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.A²t.desc"/> @@ -549,7 +557,7 @@ descID="IEC61850_7_3.DAEnums::SIUnitKind.char_per_s.desc"/> + descID="IEC61850_7_3.DAEnums::SIUnitKind.kgm².desc"/> @@ -1429,7 +1437,8 @@ type="Timestamp" dchg="true" descID="IEC61850_7_3.CDCStatusInfo::BCR.strTm.desc" - presCond="O"/> + presCond="OF" + presCondArgs="frVal"/> + presCond="MO" + presCondArgs="db"/> + presCond="MO" + presCondArgs="db"/> - + - + + release="5" + umlVersion="WG10built12" + umlDate="2024-02-14" + publicationStage="IS" + appVersion="j61850DocBuilder 02.03 based on jCleanCim noNS beta9.3 (derived from jCleanCim 02-02)" + namespaceType="basic" + nsdVersion="2017" + nsdRevision="B" + nsdRelease="5"> - COPYRIGHT (c) IEC, www.iec.ch/tc57/supportdocuments. This version of this NSD is part of IEC_61850-7-4:2007; see the IEC_61850-7-4:2007 for full legal notices. In case of any differences between the here-below code and the IEC published content, the here-below definition supersedes the IEC publication; it may contain updates. See history files. The whole document has to be taken into account to have a full description of this code component. + COPYRIGHT (c) IEC, www.iec.ch/tc57/supportdocuments. This version of this NSD is part of IEC_61850-7-4:2020 Edition 2.1; see the IEC_61850-7-4:2020 Edition 2.1 for full legal notices. In case of any differences between the here-below code and the IEC published content, the here-below definition supersedes the IEC publication; it may contain updates. See history files. The whole document has to be taken into account to have a full description of this code component. See www.iec.ch/CCv1 for copyright details. - + + @@ -953,6 +962,7 @@ + @@ -1164,11 +1174,9 @@ - - - + + + @@ -1611,7 +1619,8 @@ - + + @@ -1957,12 +1966,6 @@ - @@ -3547,6 +3554,20 @@ descID="IEC61850_7_4.LNGroupL::LTRK.SgcbTrk.desc" presCond="O" dsPresCond="na"/> + + - - - + - - - + + - + + + { - const element: Element = document.createElement(name); - element.textContent = textContent; - - return element; -}; - -/** TODO: Make this return JSON */ -export function CompasNSDocFileService() { - return { - listNsdocFiles(): Promise { - const document: XMLDocument = new DOMParser().parseFromString( - '', - 'text/xml' - ); +interface NsDocFileInfo { + id: string; + version: string; + revision: string; + release: string; + filename: string; + fullVersion: string; +} + +interface NsDocFileResponse { + id: string; + nsdocId: string; + filename: string; + checksum: string; +} - nsDocfiles.forEach(nsDocFile => { - const nsDocFileElement: Element = document.createElement('NsdocFile'); +interface NsDocListResponse { + files: NsDocFileResponse[]; +} - nsDocFileElement.appendChild( - createElement('Id', nsDocFile.id, document) - ); - nsDocFileElement.appendChild( - createElement('NsdocId', nsDocFile.name, document) - ); - nsDocFileElement.appendChild( - createElement('Checksum', nsDocFile.id, document) - ); - nsDocFileElement.appendChild( - createElement('Filename', nsDocFile.filename, document) +interface NsdocContentResponse { + content: string; +} + +function generateIdFromName(name: string): string { + let hash = 0; + for (let i = 0; i < name.length; i++) { + const char = name.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + const hashStr = Math.abs(hash).toString(16).padStart(8, '0'); + return `${hashStr.slice(0, 8)}-${hashStr.slice(0, 4)}-4${hashStr.slice( + 1, + 4 + )}-${hashStr.slice(0, 4)}-${hashStr}${hashStr.slice(0, 4)}`; +} + +function parseVersionNumber( + version: string, + revision: string, + release: string +): number { + const versionNum = parseInt(version) || 0; + const revisionNum = revision.charCodeAt(0) - 65; + const releaseNum = parseInt(release) || 0; + + return versionNum * 1000000 + revisionNum * 1000 + releaseNum; +} + +function parseNsDocFilename(filename: string): NsDocFileInfo | null { + const match = filename.match( + /^IEC_61850-([0-9]+-[0-9]+)_(\d{4})([A-Z])(\d+)-en\.nsdoc$/ + ); + if (match) { + const [, standardPart, version, revision, release] = match; + const id = `IEC 61850-${standardPart}`; + const fullVersion = `${version}${revision}${release}`; + + return { + id, + version, + revision, + release, + filename, + fullVersion, + }; + } + return null; +} + +async function isValidNsDocFile(filename: string): Promise { + try { + const response = await fetch(`/public/nsdoc/${filename}`); + if (!response.ok) { + return false; + } + + const content = await response.text(); + const doc = new DOMParser().parseFromString(content, 'text/xml'); + const nsElement = doc.querySelector('NSDoc'); + const xmlns = nsElement?.getAttribute('xmlns'); + + return xmlns === 'http://www.iec.ch/61850/2016/NSD'; + } catch (error) { + return false; + } +} + +// Get NSDOC files from manifest.json +async function getNsDocFilesFromManifest(): Promise { + try { + const manifestResponse = await fetch('/public/nsdoc/manifest.json'); + if (!manifestResponse.ok) { + return []; + } + + const manifest = await manifestResponse.json(); + if (!Array.isArray(manifest)) { + return []; + } + + const nsdocFiles = manifest.filter( + (filename: unknown) => + typeof filename === 'string' && filename.endsWith('-en.nsdoc') + ) as string[]; + + return nsdocFiles; + } catch (error) { + return []; + } +} + +// Discover NSDOC files using pattern-based approach (fallback) +async function getNsDocFilesByPattern(): Promise { + const discoveredFiles: string[] = []; + + const standards2007 = ['7-2', '7-3', '7-4']; + for (const standard of standards2007) { + for (let release = 5; release <= 9; release++) { + const filename = `IEC_61850-${standard}_2007B${release}-en.nsdoc`; + try { + const response = await fetch(`/public/nsdoc/${filename}`); + if (response.ok) { + discoveredFiles.push(filename); + break; + } + } catch (e) { + // Continue to next version + } + } + } + + for (let release = 2; release <= 9; release++) { + const filename = `IEC_61850-8-1_2003A${release}-en.nsdoc`; + try { + const response = await fetch(`/public/nsdoc/${filename}`); + if (response.ok) { + discoveredFiles.push(filename); + break; + } + } catch (e) { + // Continue to next version + } + } + + return discoveredFiles; +} + +async function parseAndValidateNsDocFiles( + filenames: string[] +): Promise { + const parsedFiles: NsDocFileInfo[] = []; + + for (const filename of filenames) { + const fileInfo = parseNsDocFilename(filename); + if (fileInfo) { + const isValid = await isValidNsDocFile(filename); + if (isValid) { + parsedFiles.push(fileInfo); + } else { + console.warn( + `Skipping invalid NSDOC file: ${filename} (missing or incorrect schema)` ); + } + } + } + + return parsedFiles; +} - document - .querySelector('NsdocListResponse')! - .appendChild(nsDocFileElement); - }); +function selectLatestVersions(parsedFiles: NsDocFileInfo[]): NsDocFile[] { + const standardsMap = new Map(); + + for (const fileInfo of parsedFiles) { + const currentFileInMap = standardsMap.get(fileInfo.id); + + if (!currentFileInMap) { + standardsMap.set(fileInfo.id, fileInfo); + } else { + const currentVersionNum = parseVersionNumber( + currentFileInMap.version, + currentFileInMap.revision, + currentFileInMap.release + ); + const newVersionNum = parseVersionNumber( + fileInfo.version, + fileInfo.revision, + fileInfo.release + ); - return Promise.resolve(document); + if (newVersionNum > currentVersionNum) { + standardsMap.set(fileInfo.id, fileInfo); + } + } + } + + return Array.from(standardsMap.values()).map(fileInfo => ({ + filename: fileInfo.filename, + name: fileInfo.id, + id: generateIdFromName(fileInfo.id + fileInfo.fullVersion), + })); +} + +async function getNsDocFiles(): Promise { + try { + let nsdocFiles = await getNsDocFilesFromManifest(); + if (nsdocFiles.length === 0) { + nsdocFiles = await getNsDocFilesByPattern(); + } + + if (nsdocFiles.length === 0) { + console.warn( + 'No NSDOC files found using either manifest or pattern-based discovery' + ); + return []; + } + + const parsedFiles = await parseAndValidateNsDocFiles(nsdocFiles); + + return selectLatestVersions(parsedFiles); + } catch (error) { + console.error('Failed to load NSDOC files:', error); + return []; + } +} + +export function CompasNSDocFileService(): { + listNsdocFiles(): Promise; + getNsdocFile(id: string): Promise; +} { + return { + async listNsdocFiles(): Promise { + const nsDocFiles = await getNsDocFiles(); + + return { + files: nsDocFiles.map((nsDocFile: NsDocFile) => ({ + id: nsDocFile.id, + nsdocId: nsDocFile.name, + filename: nsDocFile.filename, + checksum: nsDocFile.id, + })), + }; }, - getNsdocFile(id: string): Promise { - const nsDocFile: NsDocFile = nsDocfiles.find(f => f.id === id)!; + async getNsdocFile(id: string): Promise { + const nsDocFiles = await getNsDocFiles(); + const nsDocFile: NsDocFile | undefined = nsDocFiles.find( + (f: NsDocFile) => f.id === id + ); if (!nsDocFile) { return Promise.reject(`Unable to find nsDoc file with id ${id}`); } - return fetch(`./public/nsdoc/${nsDocFile.filename}`) + + const content = await fetch(`/public/nsdoc/${nsDocFile.filename}`) .catch(handleError) - .then(handleResponse) - .then(res => { - const document: XMLDocument = new DOMParser().parseFromString( - '', - 'text/xml' - ); - - document - .querySelector('NsdocResponse')! - .appendChild(createElement('NsdocFile', res, document)); - - return document; - }); + .then(handleResponse); + + return { + content, + }; }, }; } diff --git a/packages/compas-open-scd/src/compas/CompasNsdoc.ts b/packages/compas-open-scd/src/compas/CompasNsdoc.ts index f9ecb37875..eb2bb91b9e 100644 --- a/packages/compas-open-scd/src/compas/CompasNsdoc.ts +++ b/packages/compas-open-scd/src/compas/CompasNsdoc.ts @@ -4,10 +4,24 @@ import { createLogEvent, createNSDocLogEvent, } from '../compas-services/foundation.js'; -import { CompasSclValidatorService } from '../compas-services/CompasValidatorService.js'; import { CompasNSDocFileService } from '../compas-services/CompasNSDocFileService.js'; import { newLoadNsdocEvent } from '@openscd/core/foundation/deprecated/settings.js'; +interface NsdocFileResponse { + id: string; + nsdocId: string; + filename: string; + checksum: string; +} + +interface NsdocListResponse { + files: NsdocFileResponse[]; +} + +interface NsdocContentResponse { + content: string; +} + /** * Load a single entry. Use the nsdocId to look in the Local Storage, if already loaded, * and if the checksum is the same. @@ -37,10 +51,8 @@ async function processNsdocFile( console.info(`Loading NSDoc File '${nsdocId}' with ID '${id}'.`); await CompasNSDocFileService() .getNsdocFile(id) - .then(document => { - const nsdocContent = - document.querySelectorAll('NsdocFile').item(0).textContent ?? ''; - component.dispatchEvent(newLoadNsdocEvent(nsdocContent, filename)); + .then((response: NsdocContentResponse) => { + component.dispatchEvent(newLoadNsdocEvent(response.content, filename)); localStorage.setItem(checksumKey, checksum); }) .catch(() => { @@ -58,18 +70,11 @@ async function processNsdocFile( export async function loadNsdocFiles(component: Element): Promise { await CompasNSDocFileService() .listNsdocFiles() - .then(response => { - Array.from(response.querySelectorAll('NsdocFile') ?? []).forEach( - nsdocFile => { - const id = nsdocFile.querySelector('Id')!.textContent ?? ''; - const nsdocId = nsdocFile.querySelector('NsdocId')!.textContent ?? ''; - const filename = - nsdocFile.querySelector('Filename')!.textContent ?? ''; - const checksum = - nsdocFile.querySelector('Checksum')!.textContent ?? ''; - processNsdocFile(component, id, nsdocId, filename, checksum); - } - ); + .then((response: NsdocListResponse) => { + response.files.forEach(nsdocFile => { + const { id, nsdocId, filename, checksum } = nsdocFile; + processNsdocFile(component, id, nsdocId, filename, checksum); + }); }) .catch(reason => { createLogEvent(component, reason); diff --git a/packages/compas-open-scd/test/unit/compas-services/CompasNSDocFileService.test.ts b/packages/compas-open-scd/test/unit/compas-services/CompasNSDocFileService.test.ts index dedb73fe0e..d4b91a9cde 100644 --- a/packages/compas-open-scd/test/unit/compas-services/CompasNSDocFileService.test.ts +++ b/packages/compas-open-scd/test/unit/compas-services/CompasNSDocFileService.test.ts @@ -1,36 +1,250 @@ import { expect } from '@open-wc/testing'; +import { stub, restore } from 'sinon'; import { CompasNSDocFileService } from '../../../src/compas-services/CompasNSDocFileService.js'; -describe('compas-nsdocfile-service', () => { - it('Should list all NSDoc files', async () => { - const res = await CompasNSDocFileService().listNsdocFiles(); +interface NsDocFileResponse { + id: string; + nsdocId: string; + filename: string; + checksum: string; +} - const nsDocFiles: Element[] = Array.from(res.querySelectorAll('NsdocFile')); +describe('CompasNSDocFileService', () => { + let fetchStub: sinon.SinonStub; - expect(nsDocFiles.length).to.equal(4); + beforeEach(() => { + fetchStub = stub(window, 'fetch'); }); - it('Should fail on invalid request', done => { - const id = '315b02ac-c4aa-4495-9b4f-f7175a75c315'; - CompasNSDocFileService() - .getNsdocFile(id) - .then(() => done('Failed')) - .catch(err => { - expect(err.status).to.equal(404); - expect(err.type).to.equal('NotFoundError'); - done(); + afterEach(() => { + restore(); + }); + + it('should list all NSDoc files', async () => { + const mockManifest = [ + 'IEC_61850-7-2_2003A2-en.nsdoc', + 'IEC_61850-7-3_2007B3-en.nsdoc', + 'IEC_61850-8-1_2011A1-en.nsdoc', + ]; + + const validNsdocContent = ` + + Test + `; + + fetchStub.callsFake((url: string) => { + if (url.includes('manifest.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockManifest), + }); + } + + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(validNsdocContent), + }); + }); + + const service = CompasNSDocFileService(); + const result = await service.listNsdocFiles(); + + expect(result).to.have.property('files'); + expect(result.files).to.be.an('array'); + expect(result.files.length).to.equal(3); + + result.files.forEach((file: NsDocFileResponse) => { + expect(file).to.have.property('id'); + expect(file).to.have.property('nsdocId'); + expect(file).to.have.property('filename'); + expect(file).to.have.property('checksum'); + expect(file.filename).to.match( + /^IEC_61850-[0-9]+-[0-9]+_\d{4}[A-Z]\d+-en\.nsdoc$/ + ); + }); + }); + + it('should get NSDOC file content', async () => { + const mockManifest = ['IEC_61850-7-2_2003A2-en.nsdoc']; + const validNsdocContent = ` + + Test + `; + + fetchStub.callsFake((url: string) => { + if (url.includes('manifest.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockManifest), + }); + } + + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(validNsdocContent), }); + }); + + const service = CompasNSDocFileService(); + + const listResult = await service.listNsdocFiles(); + const firstFile = listResult.files[0]; + const contentResult = await service.getNsdocFile(firstFile.id); + const doc = new DOMParser().parseFromString( + contentResult.content, + 'text/xml' + ); + const nsElement = doc.querySelector('NSDoc'); + + expect(contentResult).to.have.property('content'); + expect(nsElement).to.not.be.null; + expect(nsElement?.getAttribute('xmlns')).to.equal( + 'http://www.iec.ch/61850/2016/NSD' + ); + expect(nsElement?.getAttribute('id')).to.equal('IEC 61850-7-2'); + + const docElement = doc.querySelector('Doc'); + expect(docElement).to.not.be.null; + expect(docElement?.getAttribute('id')).to.equal('IEC61850-7-2'); }); - it('Should fail on invalid id', done => { - const id = '1'; - - CompasNSDocFileService() - .getNsdocFile(id) - .then(() => done('Failed')) - .catch(err => { - expect(err).to.equal(`Unable to find nsDoc file with id ${id}`); - done(); + + it('should fail on missing file', async () => { + fetchStub.callsFake((url: string) => { + if (url.includes('manifest.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve([]), + }); + } + + return Promise.resolve({ + ok: false, + status: 404, }); + }); + + const service = CompasNSDocFileService(); + const invalidId = 'invalid-id-123'; + + try { + await service.getNsdocFile(invalidId); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.equal(`Unable to find nsDoc file with id ${invalidId}`); + } + }); + + it('should handle version selection correctly', async () => { + const mockManifest = [ + 'IEC_61850-7-2_2003A2-en.nsdoc', + 'IEC_61850-7-2_2003B1-en.nsdoc', + 'IEC_61850-8-1_2011A1-en.nsdoc', + 'IEC_61850-8-1_2011B2-en.nsdoc', + ]; + + const validNsdocContent72 = ` + + Test 7-2 + `; + + const validNsdocContent81 = ` + + Test 8-1 + `; + + fetchStub.callsFake((url: string) => { + if (url.includes('manifest.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockManifest), + }); + } + + const filename = url.split('/').pop() || ''; + if (filename.includes('7-2')) { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(validNsdocContent72), + }); + } else if (filename.includes('8-1')) { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(validNsdocContent81), + }); + } + + return Promise.resolve({ + ok: false, + status: 404, + }); + }); + + const service = CompasNSDocFileService(); + const result = await service.listNsdocFiles(); + + const filesByStandard = new Map(); + result.files.forEach((file: NsDocFileResponse) => { + if (!filesByStandard.has(file.nsdocId)) { + filesByStandard.set(file.nsdocId, []); + } + filesByStandard.get(file.nsdocId)!.push(file); + }); + + filesByStandard.forEach((files, standardId) => { + expect(files).to.have.length( + 1, + `Standard ${standardId} should have only one file (latest version)` + ); + }); + }); + + it('should handle schema validation correctly', async () => { + const mockManifest = [ + 'IEC_61850-7-2_2003A2-en.nsdoc', + 'IEC_61850-INVALID_2023A1-en.nsdoc', + ]; + + const validNsdocContent = ` + + Test + `; + + const invalidNsdocContent = ` + + Invalid + `; + + fetchStub.callsFake((url: string) => { + if (url.includes('manifest.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockManifest), + }); + } + + const filename = url.split('/').pop() || ''; + if (filename.includes('INVALID')) { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(invalidNsdocContent), + }); + } + + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(validNsdocContent), + }); + }); + + const service = CompasNSDocFileService(); + const result = await service.listNsdocFiles(); + + expect(result.files).to.be.an('array'); + + const invalidFile = result.files.find((f: NsDocFileResponse) => + f.filename.includes('INVALID') + ); + expect(invalidFile).to.be.undefined; }); });