Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- For released and deprecated trafos, the wiring dialog now has a button to reset the wiring to the release wiring.
- extend API documentation page to include maintenance endpoints and Blob storage adapter

## 0.14.6
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { BackendApi } from '../fixtures/backend-api';
import { expect, test } from '../fixtures/fixture';

const componentCategory = 'Test';
const componentTag = '0.1.0';
const inputName = 'input';
const outputName = 'output';
const releaseValue = 'release value';
const changedValue = 'changed value';

const wiringWithValue = (value: string) => ({
input_wirings: [
{
workflow_input_name: inputName,
adapter_id: 'direct_provisioning',
filters: { value }
}
],
output_wirings: [] as unknown[]
});

// Releasing stores the test wiring as release wiring - releasing has its own
// test, so it happens through the rest api here.
const createReleasedComponent = async (
backendApi: BackendApi,
componentName: string
): Promise<string> => {
const componentId = await backendApi.createComponent({
name: componentName,
category: componentCategory,
description: 'Resets to its release wiring',
versionTag: componentTag,
inputs: [{ name: inputName, dataType: 'STRING' }],
outputs: [{ name: outputName, dataType: 'STRING' }],
functionBody: `return {"${outputName}": ${inputName}}`,
documentation: '',
testWiring: wiringWithValue(releaseValue)
});
await backendApi.releaseComponent(componentId);
return componentId;
};

// The test wiring of a released component may still change, so the wiring
// dialog offers to go back to the release wiring.
test('Reset the test wiring of a released component to its release wiring', async ({
page,
hetidaDesigner,
backendApi,
browserName
}, testInfo) => {
// Arrange
const componentName = `Test reset to release wiring ${browserName} ${testInfo.retry}`;
const componentId = await createReleasedComponent(backendApi, componentName);
await backendApi.updateTestWiring(componentId, wiringWithValue(changedValue));

// The navigation is filled on page load, so it has to be told about the
// component that was created through the api afterwards.
await page.reload({ waitUntil: 'domcontentloaded' });

await hetidaDesigner.clickComponentsInNavigation();
await hetidaDesigner.clickCategoryInNavigation(componentCategory);
await hetidaDesigner.doubleClickItemInNavigation(
`${componentName}(${componentTag})`
);
await hetidaDesigner.openExecuteDialog(
`Execute Component ${componentName} ${componentTag}`
);

const valueInput = page.getByTestId(`${inputName}-value-input-wiring-dialog`);
await expect(valueInput).toHaveValue(changedValue);

// Act
await hetidaDesigner.clickByTestId('reset-to-release-wiring-wiring-dialog');

// Assert
await expect(valueInput).toHaveValue(releaseValue);

// Only confirming the dialog stores the wiring as test wiring.
await hetidaDesigner.clickByTestId('execute-wiring-dialog');
await expect
.poll(
async () => (await backendApi.getTransformation(componentId)).test_wiring,
{ timeout: 15000 }
)
.toMatchObject(wiringWithValue(releaseValue));
await expect(
page.locator('hd-protocol-viewer >> .protocol-content')
).toContainText(releaseValue);
});

// The wiring dialog of a schedule offers the same reset, to the release wiring
// of the scheduled component.
test('Reset the wiring of a schedule to the release wiring of its component', async ({
page,
hetidaDesigner,
backendApi,
browserName
}, testInfo) => {
// Arrange
const componentName = `Test reset schedule to release wiring ${browserName} ${testInfo.retry}`;
const componentId = await createReleasedComponent(backendApi, componentName);
const scheduleId = await backendApi.createSchedule(
componentName,
componentId,
wiringWithValue(changedValue)
);

// Schedules and the navigation are loaded on page load.
await page.reload({ waitUntil: 'domcontentloaded' });

await hetidaDesigner.clickTabInNavigation(1);
await page
.locator('.scheduling-row', { hasText: componentName })
.getByRole('button', { name: 'Edit Wiring' })
.click();
await expect(
page.locator(
`mat-dialog-container:has-text("Change Wiring — ${componentName} ${componentTag}")`
)
).toBeVisible();

const valueInput = page.getByTestId(`${inputName}-value-input-wiring-dialog`);
await expect(valueInput).toHaveValue(changedValue);

// Act
await hetidaDesigner.clickByTestId('reset-to-release-wiring-wiring-dialog');

// Assert
await expect(valueInput).toHaveValue(releaseValue);

// Only confirming the dialog stores the wiring of the schedule.
await hetidaDesigner.clickByTestId('save wiring-wiring-dialog');
await expect
.poll(async () => (await backendApi.getSchedule(scheduleId)).wiring, {
timeout: 15000
})
.toMatchObject(wiringWithValue(releaseValue));
});

// Cleanup goes through the rest api, see components-release.spec.ts.
test.afterEach(async ({ backendApi, browserName }, testInfo) => {
for (const name of [
`Test reset to release wiring ${browserName} ${testInfo.retry}`,
`Test reset schedule to release wiring ${browserName} ${testInfo.retry}`
]) {
await backendApi.deleteSchedulesByName(name);
await backendApi.deleteTransformationsByName(name);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, test } from '../fixtures/fixture';

// All wiring dialogs share one config, and the one of a schedule changes the
// text of its confirm button. Cancelling it used to leave that text behind, so
// that every execute dialog afterwards offered "Save Wiring" instead.
test('Cancelling the wiring dialog of a schedule leaves the execute dialog unchanged', async ({
page,
hetidaDesigner,
backendApi,
browserName
}, testInfo) => {
// Arrange
const componentCategory = 'Test';
const componentName = `Test cancel schedule wiring ${browserName} ${testInfo.retry}`;
const componentTag = '0.1.0';

const componentId = await backendApi.createComponent({
name: componentName,
category: componentCategory,
description: 'Is scheduled',
versionTag: componentTag,
inputs: [],
// A component without any io is incomplete and cannot be executed.
outputs: [{ name: 'output', dataType: 'INT' }],
functionBody: 'return {"output": 1}',
documentation: ''
});
await backendApi.createSchedule(componentName, componentId, {
input_wirings: [],
output_wirings: []
});

// Schedules and the navigation are loaded on page load.
await page.reload({ waitUntil: 'domcontentloaded' });

// Act
await hetidaDesigner.clickTabInNavigation(1);
await page
.locator('.scheduling-row', { hasText: componentName })
.getByRole('button', { name: 'Edit Wiring' })
.click();
await hetidaDesigner.clickByTestId('cancel-wiring-dialog');
await expect(page.locator('mat-dialog-container')).toHaveCount(0);

await hetidaDesigner.clickComponentsInNavigation();
await hetidaDesigner.clickCategoryInNavigation(componentCategory);
await hetidaDesigner.doubleClickItemInNavigation(
`${componentName}(${componentTag})`
);
await hetidaDesigner.openExecuteDialog(
`Execute Component ${componentName} ${componentTag}`
);

// Assert
await expect(page.getByTestId('execute-wiring-dialog')).toHaveText('Execute');
});

// Cleanup goes through the rest api, see components-release.spec.ts.
test.afterEach(async ({ backendApi, browserName }, testInfo) => {
const name = `Test cancel schedule wiring ${browserName} ${testInfo.retry}`;
await backendApi.deleteSchedulesByName(name);
await backendApi.deleteTransformationsByName(name);
});
107 changes: 107 additions & 0 deletions frontend/playwright/test-cases/fixtures/backend-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ interface TransformationStub {
interface Transformation extends TransformationStub {
documentation: string;
content: string;
test_wiring: unknown;
release_wiring: unknown;
}

interface Schedule {
id: string;
name: string;
active: boolean;
cron_expression: string;
transformation_id: string | null;
wiring: unknown;
}

const withTrailingSlash = (url: string): string =>
Expand Down Expand Up @@ -205,6 +216,27 @@ export class BackendApi {
}
}

/**
* Replaces the test wiring, which unlike most of a transformation may still
* change once it is released or deprecated.
*/
public async updateTestWiring(
id: string,
testWiring: unknown
): Promise<void> {
const transformation = await this.getTransformation(id);
const response = await this.request.put(
`${this.apiUrl}/transformations/${id}`,
{ data: { ...transformation, test_wiring: testWiring } }
);

if (!response.ok()) {
throw new Error(
`Could not update the test wiring of ${id}: ${response.status()} ${await response.text()}`
);
}
}

public async getTransformation(id: string): Promise<Transformation> {
const response = await this.request.get(
`${this.apiUrl}/transformations/${id}`
Expand Down Expand Up @@ -258,4 +290,79 @@ export class BackendApi {

return (await response.json()) as TransformationStub[];
}

/**
* Creates an inactive schedule, so that it never runs during a test.
*/
public async createSchedule(
name: string,
transformationId: string,
wiring: unknown
): Promise<string> {
const id = randomUUID();
const response = await this.request.post(`${this.apiUrl}/schedules`, {
data: {
id,
name,
active: false,
cron_expression: '*/5 * * * *',
transformation_id: transformationId,
wiring
}
});

if (!response.ok()) {
throw new Error(
`Could not create schedule ${name}: ${response.status()} ${await response.text()}`
);
}

return id;
}

public async getSchedule(id: string): Promise<Schedule> {
const schedule = (await this.getSchedules()).find(
candidate => candidate.id === id
);
if (schedule === undefined) {
throw new Error(`Could not find schedule ${id}`);
}

return schedule;
}

/**
* Removes every schedule with exactly this name. Does nothing when there is
* none, so it is safe to call from an afterEach hook.
*/
public async deleteSchedulesByName(name: string): Promise<void> {
const schedules = (await this.getSchedules()).filter(
schedule => schedule.name === name
);
for (const schedule of schedules) {
const response = await this.request.delete(
`${this.apiUrl}/schedules/${schedule.id}`
);

if (!response.ok()) {
throw new Error(
`Could not delete schedule ${schedule.name}, id ${schedule.id}: ` +
`${response.status()} ${response.statusText()}`
);
}
}
}

// The backend has no endpoint for a single schedule.
private async getSchedules(): Promise<Schedule[]> {
const response = await this.request.get(`${this.apiUrl}/schedules`);

if (!response.ok()) {
throw new Error(
`Could not read schedules: ${response.status()} ${response.statusText()}`
);
}

return (await response.json()) as Schedule[];
}
}
Loading
Loading