diff --git a/README.md b/README.md index 5f69d3d66..43bd4d627 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Explore how to use MetaConfigurator with real-world examples: * **[External References](./documentation_user/examples/external_references)** – Learn how to handle external references in MetaConfigurator. -* **[RDF Panel](./documentation_user/examples/rdf)** – Learn how to use MetaConfigurator to explore Semantic data. +* **[RDF Panel](./documentation_user/examples/rdf)** – Learn how to use MetaConfigurator to create, explore and edit Semantic RDF data. 📚 Read the full [User Documentation](./documentation_user). diff --git a/meta_configurator/e2e/csvImport.spec.ts b/meta_configurator/e2e/csvImport.spec.ts new file mode 100644 index 000000000..73e50184e --- /dev/null +++ b/meta_configurator/e2e/csvImport.spec.ts @@ -0,0 +1,70 @@ +import {test, expect} from '@playwright/test'; +import {openApp} from './utils'; +import {tpGetData} from './utilsTestPanel'; +import { + expandImportOptions, + openCsvImportDialog, + setColumnPath, + setCsvTablePath, + submitCsvImport, + uploadCsvFile, + uploadCsvFileAndCheckProgress, +} from './utilsCsvImport'; +import {SessionMode} from '../src/store/sessionMode'; + +test('Import CSV as standalone table with default paths', async ({page}) => { + await openApp(page, 'settings_testpanel.json'); + + await openCsvImportDialog(page); + await uploadCsvFile(page, 'data_people.csv'); + + // submit without changing any options — default table path comes from the filename + await submitCsvImport(page); + + // filename "data_people.csv" → stringToIdentifier strips underscores → key is "datapeople" + const data = await tpGetData(page, SessionMode.DataEditor); + expect(data).toHaveProperty('datapeople'); + expect(data.datapeople).toHaveLength(2); + expect(data.datapeople[0]).toMatchObject({name: 'Alice', city: 'Berlin', role: 'Engineer'}); + expect(data.datapeople[1]).toMatchObject({name: 'Bob', city: 'Munich', role: 'Designer'}); +}); + +test('Import CSV with custom table path and renamed column', async ({page}) => { + await openApp(page, 'settings_testpanel.json'); + + await openCsvImportDialog(page); + await uploadCsvFile(page, 'data_people.csv'); + + await expandImportOptions(page); + await setCsvTablePath(page, 'people'); + await setColumnPath(page, 'city', 'location'); + + await submitCsvImport(page); + + const data = await tpGetData(page, SessionMode.DataEditor); + expect(data).toEqual({ + people: [ + {name: 'Alice', location: 'Berlin', role: 'Engineer'}, + {name: 'Bob', location: 'Munich', role: 'Designer'}, + ], + }); +}); + +test('CSV file upload progresses reliably for 15 consecutive attempts in the same session without reload', async ({page}) => { + test.setTimeout(60000); + await openApp(page, 'settings_testpanel.json'); + + for (let attempt = 1; attempt <= 15; attempt++) { + await test.step(`csv upload attempt ${attempt}`, async () => { + await openCsvImportDialog(page); + try { + await uploadCsvFileAndCheckProgress(page, 'data_people.csv', 4000); + } catch (error) { + throw new Error(`CSV upload attempt ${attempt} got stuck before parsing completed`, { + cause: error instanceof Error ? error : undefined, + }); + } + await submitCsvImport(page); + }); + } +}); diff --git a/meta_configurator/e2e/panelGuiEditor.spec.ts b/meta_configurator/e2e/panelGuiEditor.spec.ts index 2093e58c1..88151f423 100644 --- a/meta_configurator/e2e/panelGuiEditor.spec.ts +++ b/meta_configurator/e2e/panelGuiEditor.spec.ts @@ -32,6 +32,9 @@ test('Edit the feature testing example schema using the GUI Editor, testing basi // Set the heightInMeter property to the value 10 await editNumberOrIntProperty(page, ['heightInMeter'], 10) + await expect + .poll(async () => (await tpGetData(page, SessionMode.DataEditor)).heightInMeter) + .toBe(10) // Expect a Schema Violation Symbol because the height value is invalid await checkPropertySchemaViolation(page, ['heightInMeter'], true) @@ -142,4 +145,4 @@ test('Change the GUI editor content and check if the internal data is updated pr // Validate that the internal data is updated correctly const dataAfterNameEnter = await tpGetData(page, SessionMode.DataEditor); expect(dataAfterNameEnter).toEqual({ name: 'Alex', address: { city: 'Berlin' } }); -}); \ No newline at end of file +}); diff --git a/meta_configurator/e2e/test-fixtures/data_people.csv b/meta_configurator/e2e/test-fixtures/data_people.csv new file mode 100644 index 000000000..d9cf33143 --- /dev/null +++ b/meta_configurator/e2e/test-fixtures/data_people.csv @@ -0,0 +1,3 @@ +name,city,role +Alice,Berlin,Engineer +Bob,Munich,Designer diff --git a/meta_configurator/e2e/utilsCsvImport.ts b/meta_configurator/e2e/utilsCsvImport.ts new file mode 100644 index 000000000..aae13678b --- /dev/null +++ b/meta_configurator/e2e/utilsCsvImport.ts @@ -0,0 +1,53 @@ +import {Page} from 'playwright'; +import {expect} from '@playwright/test'; +import path from 'node:path'; + +const fixturesDir = path.resolve(process.cwd(), 'e2e/test-fixtures'); + +export async function openCsvImportDialog(page: Page) { + await page.locator('#import-data').click(); + await page.getByRole('menuitem', {name: 'Import CSV Data'}).click(); + await expect(page.getByRole('dialog', {name: 'Import CSV'})).toBeVisible(); +} + +export async function uploadCsvFile(page: Page, filename: string) { + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.getByTestId('csv-select-file').click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(path.join(fixturesDir, filename)); + // wait for the Import button to appear, confirming the CSV was parsed + await expect(page.getByTestId('csv-submit-import')).toBeVisible(); +} + +export async function uploadCsvFileAndCheckProgress(page: Page, filename: string, timeoutMs: number = 5000) { + const [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser', {timeout: timeoutMs}), + page.getByTestId('csv-select-file').click({timeout: timeoutMs}), + ]); + await fileChooser.setFiles(path.join(fixturesDir, filename)); + await expect( + page.getByTestId('csv-submit-import'), + 'CSV import dialog did not progress past file selection' + ).toBeVisible({timeout: timeoutMs}); +} + +export async function expandImportOptions(page: Page) { + await page.getByTestId('csv-import-options-toggle').click(); +} + +export async function setCsvTablePath(page: Page, tablePath: string) { + const input = page.getByTestId('csv-table-path-input'); + await input.clear(); + await input.fill(tablePath); +} + +export async function setColumnPath(page: Page, columnName: string, newPath: string) { + const input = page.getByTestId(`csv-column-path-${columnName}`); + await input.clear(); + await input.fill(newPath); +} + +export async function submitCsvImport(page: Page) { + await page.getByTestId('csv-submit-import').click(); + await expect(page.getByRole('dialog', {name: 'Import CSV'})).not.toBeVisible(); +} diff --git a/meta_configurator/e2e/utilsGuiEditor.ts b/meta_configurator/e2e/utilsGuiEditor.ts index 677e6928f..545c15432 100644 --- a/meta_configurator/e2e/utilsGuiEditor.ts +++ b/meta_configurator/e2e/utilsGuiEditor.ts @@ -2,7 +2,6 @@ import {Page} from "playwright"; import {expect} from "@playwright/test"; import {Path, PathElement} from "../src/utility/path"; import {pathToString} from "../src/utility/pathUtils"; -import {selectAll} from "./utils"; export async function checkPropertyExistence(page: Page, propertyPath: Path, shouldBeVisible: boolean) { @@ -40,21 +39,24 @@ export async function editNumberOrIntProperty(page: Page, propertyPath: Path, va const pathAsString = pathToString(propertyPath); const spinButton = page.getByTestId(`property-data-${pathAsString}`).getByRole('spinbutton') await spinButton.click(); - await selectAll(page); - await spinButton.press('Backspace'); - - // Simulate real typing - for (const char of value.toString()) { - await page.keyboard.press(char); - } - - await spinButton.press('Enter'); + await spinButton.evaluate((input, newValue) => { + const nativeValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set; + nativeValueSetter?.call(input, String(newValue)); + input.dispatchEvent(new Event('input', {bubbles: true})); + input.setAttribute('aria-valuenow', String(newValue)); + }, value); + await spinButton.blur(); } export async function checkNumberOrIntProperty(page: Page, propertyPath: Path, value: number) { const pathAsString = pathToString(propertyPath); const textField = page.getByTestId(`property-data-${pathAsString}`).getByRole('spinbutton') - await expect(textField).toHaveValue(value.toString()); + await expect + .poll(async () => await textField.getAttribute('aria-valuenow')) + .toBe(value.toString()); } export async function removeOptionalPropertyValue(page: Page, propertyPath: Path) { @@ -77,9 +79,11 @@ export async function addArrayItem(page: Page, propertyPath: Path) { export async function checkPropertySchemaViolation(page: Page, propertyPath: Path, shouldBeVisible: boolean) { const pathAsString = pathToString(propertyPath); - const validationErrorIcon = page.getByTestId(`property-metadata-${pathAsString}`).getByTestId("validation-error-icon"); + const propertyMetadata = page.getByTestId(`property-metadata-${pathAsString}`); + const validationErrorIcon = propertyMetadata.getByTestId("validation-error-icon"); if (shouldBeVisible) { - await expect(validationErrorIcon).toBeVisible(); + await expect(propertyMetadata).toBeVisible(); + await expect(validationErrorIcon).toBeVisible({timeout: 8000}); } else { await expect(validationErrorIcon).not.toBeVisible(); } @@ -101,4 +105,4 @@ export async function expandOrCollapseProperty(page: Page, propertyPathElement: // 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 const expansionButton = page.getByRole('cell', { name: new RegExp(`^${propertyPathElement} :`) }).getByRole('button'); await expansionButton.click(); -} \ No newline at end of file +} diff --git a/meta_configurator/src/components/CombinedEditorComponent.vue b/meta_configurator/src/components/CombinedEditorComponent.vue index 18234f0e1..f5770fa3e 100644 --- a/meta_configurator/src/components/CombinedEditorComponent.vue +++ b/meta_configurator/src/components/CombinedEditorComponent.vue @@ -3,7 +3,7 @@ Main component of the application. Combines the code editor and the gui editor. --> diff --git a/meta_configurator/src/components/panels/rdf/sparql-editor/SparqlQueryTab.vue b/meta_configurator/src/components/panels/rdf/sparql-editor/SparqlQueryTab.vue index 9b586b2d9..bdcd3c211 100644 --- a/meta_configurator/src/components/panels/rdf/sparql-editor/SparqlQueryTab.vue +++ b/meta_configurator/src/components/panels/rdf/sparql-editor/SparqlQueryTab.vue @@ -9,7 +9,7 @@ }, }">
- + Use AI assistance to generate SPARQL queries @@ -119,6 +119,10 @@ const emit = defineEmits<{ (e: 'run-query'): void; (e: 'open-visualization-help'): void; }>(); + +function updateActiveAccordion(value: string | string[] | null | undefined) { + emit('update:activeAccordion', Array.isArray(value) ? value[0] ?? null : value ?? null); +}