Skip to content

Commit d23c5e4

Browse files
authored
fix typing errors and add CSV import tests (#963)
* fix typing errors and add CSV import tests * make tests more robust and revert some original behavior * revert behavioral changes * remove dead code and dead imports and two type fixes * apply formatting changes * update readme (mainly to trigger new e2e test run) * fixed csv import feature * fix test * possibly fix file upload bug that sometimes the upload does not work * add new file dialog utils --------- Co-authored-by: Logende <[email protected]>
1 parent 3c44f43 commit d23c5e4

87 files changed

Lines changed: 958 additions & 630 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Explore how to use MetaConfigurator with real-world examples:
114114

115115
* **[External References](./documentation_user/examples/external_references)** – Learn how to handle external references in MetaConfigurator.
116116

117-
* **[RDF Panel](./documentation_user/examples/rdf)** – Learn how to use MetaConfigurator to explore Semantic data.
117+
* **[RDF Panel](./documentation_user/examples/rdf)** – Learn how to use MetaConfigurator to create, explore and edit Semantic RDF data.
118118

119119
📚 Read the full [User Documentation](./documentation_user).
120120

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {test, expect} from '@playwright/test';
2+
import {openApp} from './utils';
3+
import {tpGetData} from './utilsTestPanel';
4+
import {
5+
expandImportOptions,
6+
openCsvImportDialog,
7+
setColumnPath,
8+
setCsvTablePath,
9+
submitCsvImport,
10+
uploadCsvFile,
11+
uploadCsvFileAndCheckProgress,
12+
} from './utilsCsvImport';
13+
import {SessionMode} from '../src/store/sessionMode';
14+
15+
test('Import CSV as standalone table with default paths', async ({page}) => {
16+
await openApp(page, 'settings_testpanel.json');
17+
18+
await openCsvImportDialog(page);
19+
await uploadCsvFile(page, 'data_people.csv');
20+
21+
// submit without changing any options — default table path comes from the filename
22+
await submitCsvImport(page);
23+
24+
// filename "data_people.csv" → stringToIdentifier strips underscores → key is "datapeople"
25+
const data = await tpGetData(page, SessionMode.DataEditor);
26+
expect(data).toHaveProperty('datapeople');
27+
expect(data.datapeople).toHaveLength(2);
28+
expect(data.datapeople[0]).toMatchObject({name: 'Alice', city: 'Berlin', role: 'Engineer'});
29+
expect(data.datapeople[1]).toMatchObject({name: 'Bob', city: 'Munich', role: 'Designer'});
30+
});
31+
32+
test('Import CSV with custom table path and renamed column', async ({page}) => {
33+
await openApp(page, 'settings_testpanel.json');
34+
35+
await openCsvImportDialog(page);
36+
await uploadCsvFile(page, 'data_people.csv');
37+
38+
await expandImportOptions(page);
39+
await setCsvTablePath(page, 'people');
40+
await setColumnPath(page, 'city', 'location');
41+
42+
await submitCsvImport(page);
43+
44+
const data = await tpGetData(page, SessionMode.DataEditor);
45+
expect(data).toEqual({
46+
people: [
47+
{name: 'Alice', location: 'Berlin', role: 'Engineer'},
48+
{name: 'Bob', location: 'Munich', role: 'Designer'},
49+
],
50+
});
51+
});
52+
53+
test('CSV file upload progresses reliably for 15 consecutive attempts in the same session without reload', async ({page}) => {
54+
test.setTimeout(60000);
55+
await openApp(page, 'settings_testpanel.json');
56+
57+
for (let attempt = 1; attempt <= 15; attempt++) {
58+
await test.step(`csv upload attempt ${attempt}`, async () => {
59+
await openCsvImportDialog(page);
60+
try {
61+
await uploadCsvFileAndCheckProgress(page, 'data_people.csv', 4000);
62+
} catch (error) {
63+
throw new Error(`CSV upload attempt ${attempt} got stuck before parsing completed`, {
64+
cause: error instanceof Error ? error : undefined,
65+
});
66+
}
67+
await submitCsvImport(page);
68+
});
69+
}
70+
});

meta_configurator/e2e/panelGuiEditor.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ test('Edit the feature testing example schema using the GUI Editor, testing basi
3232

3333
// Set the heightInMeter property to the value 10
3434
await editNumberOrIntProperty(page, ['heightInMeter'], 10)
35+
await expect
36+
.poll(async () => (await tpGetData(page, SessionMode.DataEditor)).heightInMeter)
37+
.toBe(10)
3538

3639
// Expect a Schema Violation Symbol because the height value is invalid
3740
await checkPropertySchemaViolation(page, ['heightInMeter'], true)
@@ -142,4 +145,4 @@ test('Change the GUI editor content and check if the internal data is updated pr
142145
// Validate that the internal data is updated correctly
143146
const dataAfterNameEnter = await tpGetData(page, SessionMode.DataEditor);
144147
expect(dataAfterNameEnter).toEqual({ name: 'Alex', address: { city: 'Berlin' } });
145-
});
148+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name,city,role
2+
Alice,Berlin,Engineer
3+
Bob,Munich,Designer
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {Page} from 'playwright';
2+
import {expect} from '@playwright/test';
3+
import path from 'node:path';
4+
5+
const fixturesDir = path.resolve(process.cwd(), 'e2e/test-fixtures');
6+
7+
export async function openCsvImportDialog(page: Page) {
8+
await page.locator('#import-data').click();
9+
await page.getByRole('menuitem', {name: 'Import CSV Data'}).click();
10+
await expect(page.getByRole('dialog', {name: 'Import CSV'})).toBeVisible();
11+
}
12+
13+
export async function uploadCsvFile(page: Page, filename: string) {
14+
const fileChooserPromise = page.waitForEvent('filechooser');
15+
await page.getByTestId('csv-select-file').click();
16+
const fileChooser = await fileChooserPromise;
17+
await fileChooser.setFiles(path.join(fixturesDir, filename));
18+
// wait for the Import button to appear, confirming the CSV was parsed
19+
await expect(page.getByTestId('csv-submit-import')).toBeVisible();
20+
}
21+
22+
export async function uploadCsvFileAndCheckProgress(page: Page, filename: string, timeoutMs: number = 5000) {
23+
const [fileChooser] = await Promise.all([
24+
page.waitForEvent('filechooser', {timeout: timeoutMs}),
25+
page.getByTestId('csv-select-file').click({timeout: timeoutMs}),
26+
]);
27+
await fileChooser.setFiles(path.join(fixturesDir, filename));
28+
await expect(
29+
page.getByTestId('csv-submit-import'),
30+
'CSV import dialog did not progress past file selection'
31+
).toBeVisible({timeout: timeoutMs});
32+
}
33+
34+
export async function expandImportOptions(page: Page) {
35+
await page.getByTestId('csv-import-options-toggle').click();
36+
}
37+
38+
export async function setCsvTablePath(page: Page, tablePath: string) {
39+
const input = page.getByTestId('csv-table-path-input');
40+
await input.clear();
41+
await input.fill(tablePath);
42+
}
43+
44+
export async function setColumnPath(page: Page, columnName: string, newPath: string) {
45+
const input = page.getByTestId(`csv-column-path-${columnName}`);
46+
await input.clear();
47+
await input.fill(newPath);
48+
}
49+
50+
export async function submitCsvImport(page: Page) {
51+
await page.getByTestId('csv-submit-import').click();
52+
await expect(page.getByRole('dialog', {name: 'Import CSV'})).not.toBeVisible();
53+
}

meta_configurator/e2e/utilsGuiEditor.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {Page} from "playwright";
22
import {expect} from "@playwright/test";
33
import {Path, PathElement} from "../src/utility/path";
44
import {pathToString} from "../src/utility/pathUtils";
5-
import {selectAll} from "./utils";
65

76

87
export async function checkPropertyExistence(page: Page, propertyPath: Path, shouldBeVisible: boolean) {
@@ -40,21 +39,24 @@ export async function editNumberOrIntProperty(page: Page, propertyPath: Path, va
4039
const pathAsString = pathToString(propertyPath);
4140
const spinButton = page.getByTestId(`property-data-${pathAsString}`).getByRole('spinbutton')
4241
await spinButton.click();
43-
await selectAll(page);
44-
await spinButton.press('Backspace');
45-
46-
// Simulate real typing
47-
for (const char of value.toString()) {
48-
await page.keyboard.press(char);
49-
}
50-
51-
await spinButton.press('Enter');
42+
await spinButton.evaluate((input, newValue) => {
43+
const nativeValueSetter = Object.getOwnPropertyDescriptor(
44+
window.HTMLInputElement.prototype,
45+
'value'
46+
)?.set;
47+
nativeValueSetter?.call(input, String(newValue));
48+
input.dispatchEvent(new Event('input', {bubbles: true}));
49+
input.setAttribute('aria-valuenow', String(newValue));
50+
}, value);
51+
await spinButton.blur();
5252
}
5353

5454
export async function checkNumberOrIntProperty(page: Page, propertyPath: Path, value: number) {
5555
const pathAsString = pathToString(propertyPath);
5656
const textField = page.getByTestId(`property-data-${pathAsString}`).getByRole('spinbutton')
57-
await expect(textField).toHaveValue(value.toString());
57+
await expect
58+
.poll(async () => await textField.getAttribute('aria-valuenow'))
59+
.toBe(value.toString());
5860
}
5961

6062
export async function removeOptionalPropertyValue(page: Page, propertyPath: Path) {
@@ -77,9 +79,11 @@ export async function addArrayItem(page: Page, propertyPath: Path) {
7779

7880
export async function checkPropertySchemaViolation(page: Page, propertyPath: Path, shouldBeVisible: boolean) {
7981
const pathAsString = pathToString(propertyPath);
80-
const validationErrorIcon = page.getByTestId(`property-metadata-${pathAsString}`).getByTestId("validation-error-icon");
82+
const propertyMetadata = page.getByTestId(`property-metadata-${pathAsString}`);
83+
const validationErrorIcon = propertyMetadata.getByTestId("validation-error-icon");
8184
if (shouldBeVisible) {
82-
await expect(validationErrorIcon).toBeVisible();
85+
await expect(propertyMetadata).toBeVisible();
86+
await expect(validationErrorIcon).toBeVisible({timeout: 8000});
8387
} else {
8488
await expect(validationErrorIcon).not.toBeVisible();
8589
}
@@ -101,4 +105,4 @@ export async function expandOrCollapseProperty(page: Page, propertyPathElement:
101105
// do check if the name starts with the propertyName, but ignore the other part of the name, as it can differ always depending on the children count
102106
const expansionButton = page.getByRole('cell', { name: new RegExp(`^${propertyPathElement} :`) }).getByRole('button');
103107
await expansionButton.click();
104-
}
108+
}

meta_configurator/src/components/CombinedEditorComponent.vue

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Main component of the application.
33
Combines the code editor and the gui editor.
44
-->
55
<script lang="ts" setup>
6-
import {computed, onMounted, onUnmounted, type Ref, ref, watch} from 'vue';
6+
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
77
import 'primeicons/primeicons.css';
88
import SplitterPanel from 'primevue/splitterpanel';
99
import Splitter from 'primevue/splitter';
@@ -15,13 +15,13 @@ import {useConfirm} from 'primevue/useconfirm';
1515
import {confirmationService} from '@/utility/confirmationService';
1616
import {toastService} from '@/utility/toastService';
1717
import {useAppRouter} from '@/router/router';
18-
import {useDropZone, useWindowSize, watchImmediate} from '@vueuse/core/index';
18+
import {useDropZone, useWindowSize, watchImmediate} from '@vueuse/core';
1919
import {readFileContentToDataLink} from '@/utility/readFileContent';
2020
import {getDataForMode} from '@/data/useDataLink';
2121
import {useSettings} from '@/settings/useSettings';
2222
import {modeToRoute, SessionMode} from '@/store/sessionMode';
2323
import {useSessionStore} from '@/store/sessionStore';
24-
import type {SettingsInterfacePanels, SettingsInterfaceRoot} from '@/settings/settingsTypes';
24+
import type {SettingsInterfacePanels} from '@/settings/settingsTypes';
2525
import {SETTINGS_DATA_DEFAULT} from '@/settings/defaultSettingsData';
2626
import {updateSettingsWithDefaults} from '@/settings/settingsUpdater';
2727
import {panelTypeRegistry} from '@/components/panels/panelTypeRegistry';
@@ -38,15 +38,17 @@ let panelsDefinition: SettingsInterfacePanels = settings.value.panels;
3838
// Ace Editor
3939
watchImmediate(
4040
() => settings,
41-
(settings: Ref<SettingsInterfaceRoot>) => {
42-
let panels = settings.value.panels;
41+
settings => {
42+
const panels = settings.value.panels;
4343
if (JSON.stringify(panels) !== JSON.stringify(panelsDefinition)) {
4444
panelsDefinition = panels;
4545
}
4646
// fix panels if they are not defined
4747
for (let mode of Object.values(SessionMode)) {
4848
if (!panels[mode]) {
49-
panels[mode] = structuredClone(SETTINGS_DATA_DEFAULT.panels[mode]);
49+
panels[mode] = structuredClone(
50+
SETTINGS_DATA_DEFAULT.panels[mode]
51+
) as SettingsInterfacePanels[typeof mode];
5052
}
5153
}
5254
}
@@ -61,6 +63,7 @@ const panels = computed(() => {
6163
};
6264
});
6365
});
66+
const panelsKey = computed(() => JSON.stringify(panels.value));
6467
6568
let {width} = useWindowSize();
6669
@@ -164,10 +167,10 @@ onUnmounted(() => {
164167
class="h-full"
165168
style="min-width: 0"
166169
:layout="width < 600 ? 'vertical' : 'horizontal'"
167-
:key="panels">
170+
:key="panelsKey">
168171
<SplitterPanel
169172
v-for="(panel, index) in panels"
170-
:key="index + panel"
173+
:key="`${panel.sessionMode}-${index}`"
171174
:min-size="10"
172175
:size="panel.size"
173176
:resizable="true">

meta_configurator/src/components/panels/ai-prompts/AiPromptsTemplate.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,10 @@ async function submitPromptExportDocument() {
261261
// if the schema defines export formats, the user prompt is ignored and the selected export format is used
262262
if (documentExportFormatNames.value.length > 0) {
263263
const exportFormatName = selectedExportFormat.value;
264-
const exportFormatDef = documentExportFormats.value![exportFormatName];
264+
const exportFormatDef = documentExportFormats.value?.[exportFormatName];
265+
if (!exportFormatDef) {
266+
throw new Error(`Unknown export format "${exportFormatName}".`);
267+
}
265268
// if export format is just a string, it is the URL
266269
if (typeof exportFormatDef === 'string') {
267270
userPrompt = await fetchExternalContentText(exportFormatDef);

meta_configurator/src/components/panels/code-editor/setupAnnotations.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ function validationErrorsToAnnotations(editor: Editor, errors: ErrorObject[]): A
7777
}
7878

7979
const index = positions[instancePathKey];
80-
let position;
80+
if (index === undefined) {
81+
continue;
82+
}
83+
let position: {row: number; column: number};
8184
if (index in cachedPositionsForIndices) {
82-
position = cachedPositionsForIndices[index];
85+
position = cachedPositionsForIndices[index]!;
8386
} else {
8487
position = editor.session.doc.indexToPosition(index, 0);
8588
cachedPositionsForIndices[index] = position;

meta_configurator/src/components/panels/code-editor/setupLinkToSelectionAndData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@/components/panels/code-editor/aceUtility';
1010
import type {SessionMode} from '@/store/sessionMode';
1111
import {getDataForMode, getSessionForMode} from '@/data/useDataLink';
12-
import {watchImmediate} from '@vueuse/core/index';
12+
import {watchImmediate} from '@vueuse/core';
1313
import {useSettings} from '@/settings/useSettings';
1414
import {sizeOf} from '@/utility/sizeOf';
1515

0 commit comments

Comments
 (0)