Skip to content

Commit 331e4d8

Browse files
Merge pull request #10 from com-pas/feature/copy-data-set-v2
feat: add copy DataSet to other IEDs
2 parents 5eb10e9 + ca158a5 commit 331e4d8

11 files changed

Lines changed: 449 additions & 7 deletions

.github/workflows/update-screenshots.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ name: Update Screenshots
22

33
on: workflow_dispatch
44

5+
permissions:
6+
contents: write
7+
58
jobs:
69
test:
710
runs-on: ubuntu-22.04
811
steps:
912
- name: Checkout
1013
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
14+
with:
15+
fetch-depth: 0
1116

1217
- name: Set up Node.js
1318
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
@@ -35,3 +40,18 @@ jobs:
3540
path: |
3641
screenshots/*/failed/
3742
screenshots/*/baseline/
43+
44+
- name: Commit updated screenshots
45+
if: failure()
46+
run: |
47+
git config user.name "github-actions[bot]"
48+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
49+
50+
git add screenshots/
51+
52+
git diff --cached --quiet && echo "No screenshot changes" && exit 0
53+
54+
git commit -m "test: update visual baselines"
55+
git push
56+
57+

editors/dataset/data-set-editor.spec.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
isRemove,
1212
} from '@openenergytools/scl-lib/dist/foundation/utils.js';
1313

14-
import { dataSetDoc } from './data-set-editor.testfiles.js';
14+
import { dataSetDoc, dataSetCopyDoc } from './data-set-editor.testfiles.js';
1515

1616
import { DataSetEditor } from './data-set-editor.js';
1717

@@ -140,3 +140,124 @@ describe('DataSet editor component', () => {
140140
expect(actionList.searchValue).to.equal('IED1');
141141
});
142142
});
143+
144+
describe('DataSet copy', () => {
145+
const copyDoc = new DOMParser().parseFromString(
146+
dataSetCopyDoc,
147+
'application/xml'
148+
);
149+
150+
let editor: DataSetEditor;
151+
let editEvent: SinonSpy;
152+
153+
beforeEach(async () => {
154+
editor = await fixture(
155+
html`<data-set-editor .doc="${copyDoc}"></data-set-editor>`
156+
);
157+
editEvent = spy();
158+
window.addEventListener('oscd-edit-v2', editEvent);
159+
});
160+
161+
afterEach(() => {
162+
window.removeEventListener('oscd-edit-v2', editEvent);
163+
});
164+
165+
it('determines CanCopy status when target IED has matching structure and no conflict', () => {
166+
const dataSet = copyDoc.querySelector(
167+
'IED[name="IED"] DataSet[name="datSet"]'
168+
)!;
169+
const ied2 = copyDoc.querySelector('IED[name="IED2"]')!;
170+
171+
const status = (editor as any).getDataSetCopyStatus(dataSet, ied2);
172+
expect(status).to.equal('CanCopy');
173+
});
174+
175+
it('determines DataSetAlreadyExists when target IED has a DataSet with the same name', () => {
176+
const dataSet = copyDoc.querySelector(
177+
'IED[name="IED"] DataSet[name="datSet"]'
178+
)!;
179+
const ied3 = copyDoc.querySelector('IED[name="IED3"]')!;
180+
181+
const status = (editor as any).getDataSetCopyStatus(dataSet, ied3);
182+
expect(status).to.equal('DataSetAlreadyExists');
183+
});
184+
185+
it('determines IEDStructureIncompatible when target IED has no matching LDevice', () => {
186+
const dataSet = copyDoc.querySelector(
187+
'IED[name="IED"] DataSet[name="datSet"]'
188+
)!;
189+
const ied4 = copyDoc.querySelector('IED[name="IED4"]')!;
190+
191+
const status = (editor as any).getDataSetCopyStatus(dataSet, ied4);
192+
expect(status).to.equal('IEDStructureIncompatible');
193+
});
194+
195+
it('pre-selects only CanCopy IEDs when copy dialog is opened', async () => {
196+
const dataSet = copyDoc.querySelector(
197+
'IED[name="IED"] DataSet[name="datSet"]'
198+
)!;
199+
200+
// Simulate the folder_copy callback by setting copy options directly
201+
editor.dataSetCopyOptions = ['IED2', 'IED3', 'IED4'].map(name => {
202+
const ied = copyDoc.querySelector(`IED[name="${name}"]`)!;
203+
const status = (editor as any).getDataSetCopyStatus(dataSet, ied);
204+
return { ied, dataSet, status, selected: status === 'CanCopy' };
205+
});
206+
await editor.updateComplete;
207+
208+
const ied2Option = editor.dataSetCopyOptions.find(
209+
o => o.ied.getAttribute('name') === 'IED2'
210+
);
211+
const ied3Option = editor.dataSetCopyOptions.find(
212+
o => o.ied.getAttribute('name') === 'IED3'
213+
);
214+
const ied4Option = editor.dataSetCopyOptions.find(
215+
o => o.ied.getAttribute('name') === 'IED4'
216+
);
217+
218+
expect(ied2Option?.selected).to.be.true;
219+
expect(ied3Option?.selected).to.be.false;
220+
expect(ied4Option?.selected).to.be.false;
221+
});
222+
223+
it('dispatches an insert edit event for each selected IED when copying', async () => {
224+
const dataSet = copyDoc.querySelector(
225+
'IED[name="IED"] DataSet[name="datSet"]'
226+
)!;
227+
const ied2 = copyDoc.querySelector('IED[name="IED2"]')!;
228+
229+
// Set up copy options with IED2 selected (CanCopy)
230+
editor.dataSetCopyOptions = [
231+
{ ied: ied2, dataSet, status: 'CanCopy' as any, selected: true },
232+
];
233+
await editor.updateComplete;
234+
235+
(editor as any).copyDataSet();
236+
237+
expect(editEvent).to.have.been.calledOnce;
238+
239+
const edits = editEvent.args[0][0].detail.edit;
240+
expect(edits).to.be.an('array').with.lengthOf(1);
241+
expect(edits[0]).to.satisfy(isInsert);
242+
expect(edits[0].parent.tagName).to.equal('LN0');
243+
expect(edits[0].node.tagName).to.equal('DataSet');
244+
expect(edits[0].node.getAttribute('name')).to.equal('datSet');
245+
});
246+
247+
it('does not dispatch an edit event when no IED is selected', async () => {
248+
const dataSet = copyDoc.querySelector(
249+
'IED[name="IED"] DataSet[name="datSet"]'
250+
)!;
251+
const ied2 = copyDoc.querySelector('IED[name="IED2"]')!;
252+
253+
// All options deselected
254+
editor.dataSetCopyOptions = [
255+
{ ied: ied2, dataSet, status: 'CanCopy' as any, selected: false },
256+
];
257+
await editor.updateComplete;
258+
259+
(editor as any).copyDataSet();
260+
261+
expect(editEvent).to.not.have.been.called;
262+
});
263+
});

editors/dataset/data-set-editor.testfiles.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,89 @@ export const dataSetDocWithDescs = `
349349
</DataTypeTemplates>
350350
</SCL>
351351
`;
352+
353+
/**
354+
* Document for testing DataSet copy functionality.
355+
*
356+
* IED — source; has "datSet" in ldInst1/LN0 with FCDA pointing to MMXU LN (lnInst="1")
357+
* IED2 — CanCopy: matching ldInst1/LN0, same MMXU LN, no conflicting DataSet
358+
* IED3 — DataSetAlreadyExists: matching ldInst1/LN0 that already contains "datSet"
359+
* IED4 — IEDStructureIncompatible: no ldInst1 LDevice at all
360+
*/
361+
export const dataSetCopyDoc = `
362+
<SCL>
363+
<IED name="IED">
364+
<AccessPoint name="AP1">
365+
<Server>
366+
<LDevice inst="ldInst1">
367+
<LN0 lnClass="LLN0" inst="" lnType="LLN0">
368+
<DataSet name="datSet">
369+
<FCDA ldInst="ldInst1" prefix="prefix" lnClass="MMXU" lnInst="1" doName="PhV.phsA" daName="cVal.mag.f" fc="MX" />
370+
</DataSet>
371+
</LN0>
372+
<LN prefix="prefix" lnClass="MMXU" inst="1" lnType="MMXU" />
373+
</LDevice>
374+
</Server>
375+
</AccessPoint>
376+
</IED>
377+
<IED name="IED2">
378+
<AccessPoint name="AP1">
379+
<Server>
380+
<LDevice inst="ldInst1">
381+
<LN0 lnClass="LLN0" inst="" lnType="LLN0">
382+
</LN0>
383+
<LN prefix="prefix" lnClass="MMXU" inst="1" lnType="MMXU" />
384+
</LDevice>
385+
</Server>
386+
</AccessPoint>
387+
</IED>
388+
<IED name="IED3">
389+
<AccessPoint name="AP1">
390+
<Server>
391+
<LDevice inst="ldInst1">
392+
<LN0 lnClass="LLN0" inst="" lnType="LLN0">
393+
<DataSet name="datSet">
394+
<FCDA ldInst="ldInst1" prefix="prefix" lnClass="MMXU" lnInst="1" doName="PhV.phsA" daName="cVal.mag.f" fc="MX" />
395+
</DataSet>
396+
</LN0>
397+
<LN prefix="prefix" lnClass="MMXU" inst="1" lnType="MMXU" />
398+
</LDevice>
399+
</Server>
400+
</AccessPoint>
401+
</IED>
402+
<IED name="IED4">
403+
<AccessPoint name="AP1">
404+
<Server>
405+
<LDevice inst="differentLDevice">
406+
<LN0 lnClass="LLN0" inst="" lnType="LLN0">
407+
</LN0>
408+
</LDevice>
409+
</Server>
410+
</AccessPoint>
411+
</IED>
412+
<DataTypeTemplates>
413+
<LNodeType lnClass="LLN0" id="LLN0">
414+
<DO name="Beh" type="ENS"/>
415+
</LNodeType>
416+
<LNodeType lnClass="MMXU" id="MMXU">
417+
<DO name="PhV" type="WYE"/>
418+
</LNodeType>
419+
<DOType cdc="ENS" id="ENS">
420+
<DA name="stVal" bType="Enum" fc="ST" />
421+
</DOType>
422+
<DOType cdc="WYE" id="WYE">
423+
<SDO name="phsA" type="CMV" />
424+
</DOType>
425+
<DOType cdc="CMV" id="CMV">
426+
<DA name="cVal" bType="Struct" type="Vector" fc="MX"/>
427+
<DA name="q" bType="Quality" fc="MX" />
428+
</DOType>
429+
<DAType id="Vector">
430+
<BDA name="mag" bType="Struct" type="AnalogueValue" />
431+
</DAType>
432+
<DAType id="AnalogueValue">
433+
<BDA name="f" bType="FLOAT32" />
434+
</DAType>
435+
</DataTypeTemplates>
436+
</SCL>
437+
`;

0 commit comments

Comments
 (0)