@camunda/e2e-test-suite 0.0.1442 → 0.0.1444
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/c8_versions.json +1 -0
- package/README.md +2 -0
- package/dist/fixtures/SM-8.11.d.ts +77 -0
- package/dist/fixtures/SM-8.11.js +176 -0
- package/dist/pages/8.7/ModelerCreatePage.js +21 -0
- package/dist/pages/SM-8.11/Authorization.d.ts +39 -0
- package/dist/pages/SM-8.11/Authorization.js +349 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.d.ts +8 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.js +65 -0
- package/dist/pages/SM-8.11/ClusterPage.d.ts +11 -0
- package/dist/pages/SM-8.11/ClusterPage.js +37 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.d.ts +27 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.js +187 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.d.ts +41 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.js +153 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.d.ts +11 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.js +34 -0
- package/dist/pages/SM-8.11/FormJsPage.d.ts +33 -0
- package/dist/pages/SM-8.11/FormJsPage.js +109 -0
- package/dist/pages/SM-8.11/HomePage.d.ts +20 -0
- package/dist/pages/SM-8.11/HomePage.js +49 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.d.ts +53 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.js +264 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.d.ts +92 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.js +485 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.js +78 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.d.ts +12 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.js +32 -0
- package/dist/pages/SM-8.11/KeycloakUtils.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakUtils.js +94 -0
- package/dist/pages/SM-8.11/LoginPage.d.ts +21 -0
- package/dist/pages/SM-8.11/LoginPage.js +112 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.d.ts +44 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.js +176 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.d.ts +238 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.js +1950 -0
- package/dist/pages/SM-8.11/ModelerHomePage.d.ts +97 -0
- package/dist/pages/SM-8.11/ModelerHomePage.js +956 -0
- package/dist/pages/SM-8.11/NavigationPage.d.ts +58 -0
- package/dist/pages/SM-8.11/NavigationPage.js +389 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.d.ts +10 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.js +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.d.ts +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.js +120 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.d.ts +39 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.js +154 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.js +115 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.js +116 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.d.ts +61 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.js +433 -0
- package/dist/pages/SM-8.11/OCTenantPage.d.ts +73 -0
- package/dist/pages/SM-8.11/OCTenantPage.js +399 -0
- package/dist/pages/SM-8.11/OperateHomePage.d.ts +26 -0
- package/dist/pages/SM-8.11/OperateHomePage.js +139 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.d.ts +30 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.js +276 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.js +383 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.d.ts +17 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.js +62 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.d.ts +14 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.js +63 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.d.ts +19 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.js +76 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.d.ts +50 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.js +209 -0
- package/dist/pages/SM-8.11/PlayPage.d.ts +47 -0
- package/dist/pages/SM-8.11/PlayPage.js +389 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.d.ts +35 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.js +340 -0
- package/dist/pages/SM-8.11/SettingsPage.d.ts +12 -0
- package/dist/pages/SM-8.11/SettingsPage.js +34 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.d.ts +70 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.js +273 -0
- package/dist/pages/SM-8.11/TaskPanelPage.d.ts +29 -0
- package/dist/pages/SM-8.11/TaskPanelPage.js +320 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.d.ts +15 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.js +77 -0
- package/dist/pages/SM-8.11/UtilitiesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/UtilitiesPage.js +573 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.d.ts +6 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.js +77 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.js +48 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.js +226 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.js +307 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.js +174 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.js +410 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.js +220 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.js +461 -0
- package/dist/tests/SM-8.11/login.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/login.spec.js +24 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.js +263 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.js +607 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.js +505 -0
- package/dist/tests/SM-8.11/play.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/play.spec.js +144 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.js +712 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.js +167 -0
- package/dist/tests/SM-8.11/test-setup.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/test-setup.spec.js +24 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.js +223 -0
- package/package.json +1 -1
- package/playwright.config.ts +21 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectorMarketplacePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const BrowseAllConnectorMarketplacePage_1 = require("../BrowseAllConnectorMarketplacePage");
|
|
6
|
+
const sleep_1 = require("../../utils/sleep");
|
|
7
|
+
class ConnectorMarketplacePage {
|
|
8
|
+
page;
|
|
9
|
+
browseAllConnectorMarketplacePage;
|
|
10
|
+
connectorName;
|
|
11
|
+
legacyMarketplaceDialog;
|
|
12
|
+
searchForConnectorTextbox;
|
|
13
|
+
downloadToProjectButton;
|
|
14
|
+
addToProjectButton;
|
|
15
|
+
replaceResourceButton;
|
|
16
|
+
saveAsCopyButton;
|
|
17
|
+
cancelButton;
|
|
18
|
+
marketplaceErrorMessage;
|
|
19
|
+
noResultsMessage;
|
|
20
|
+
constructor(page) {
|
|
21
|
+
this.page = page;
|
|
22
|
+
this.browseAllConnectorMarketplacePage =
|
|
23
|
+
new BrowseAllConnectorMarketplacePage_1.BrowseAllConnectorMarketplacePage(page);
|
|
24
|
+
this.searchForConnectorTextbox =
|
|
25
|
+
this.browseAllConnectorMarketplacePage.searchForConnectorTextbox
|
|
26
|
+
.or(page.getByPlaceholder('Search for a connector'))
|
|
27
|
+
.first();
|
|
28
|
+
this.legacyMarketplaceDialog = page
|
|
29
|
+
.getByRole('dialog', { name: 'Browse Marketplace connectors' })
|
|
30
|
+
.or(page.getByRole('dialog', { name: 'Discover Connectors Dialog' }))
|
|
31
|
+
.first();
|
|
32
|
+
this.downloadToProjectButton = page
|
|
33
|
+
.getByRole('button', { name: 'Download to project' })
|
|
34
|
+
.first();
|
|
35
|
+
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
36
|
+
this.addToProjectButton = page.getByRole('button', {
|
|
37
|
+
name: 'Add to project',
|
|
38
|
+
});
|
|
39
|
+
this.replaceResourceButton = page.getByRole('button', {
|
|
40
|
+
name: 'Replace resource',
|
|
41
|
+
});
|
|
42
|
+
this.saveAsCopyButton = page.getByRole('button', { name: 'Save as copy' });
|
|
43
|
+
this.marketplaceErrorMessage = page.getByText('an error occurred, please try again later');
|
|
44
|
+
this.noResultsMessage = page.getByText('find a match for your search phrase');
|
|
45
|
+
}
|
|
46
|
+
async clickSearchForConnectorTextbox() {
|
|
47
|
+
await this.searchForConnectorTextbox.click({ timeout: 60000 });
|
|
48
|
+
await (0, sleep_1.sleep)(2000);
|
|
49
|
+
}
|
|
50
|
+
async fillSearchForConnectorTextbox(connectorName) {
|
|
51
|
+
this.connectorName = connectorName;
|
|
52
|
+
await this.searchForConnectorTextbox.fill(connectorName, { timeout: 60000 });
|
|
53
|
+
await (0, sleep_1.sleep)(2000);
|
|
54
|
+
}
|
|
55
|
+
async clickDownloadToProjectButton() {
|
|
56
|
+
await this.getLegacyDownloadButton().click({ timeout: 60000 });
|
|
57
|
+
}
|
|
58
|
+
async clickCancelButton() {
|
|
59
|
+
await this.cancelButton.click({ timeout: 30000 });
|
|
60
|
+
}
|
|
61
|
+
async clickReplaceResourceButton() {
|
|
62
|
+
await this.replaceResourceButton.click({ timeout: 30000 });
|
|
63
|
+
}
|
|
64
|
+
async waitForConnectorSearchResults() {
|
|
65
|
+
if (await this.browseAllConnectorMarketplacePage.isOpen()) {
|
|
66
|
+
await this.browseAllConnectorMarketplacePage.waitForConnectorSearchResult(this.connectorName);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const MAX_RETRIES = 4;
|
|
70
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
71
|
+
const downloadButton = this.getLegacyDownloadButton();
|
|
72
|
+
await (0, test_1.expect)(downloadButton
|
|
73
|
+
.or(this.marketplaceErrorMessage)
|
|
74
|
+
.or(this.noResultsMessage)).toBeVisible({ timeout: 60000 });
|
|
75
|
+
if (await downloadButton.isVisible()) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(`Marketplace search attempt ${attempt} of ${MAX_RETRIES} returned an error or no results. Retrying search...`);
|
|
79
|
+
if (attempt < MAX_RETRIES) {
|
|
80
|
+
// The marketplace search API returns transient 5xx. Back off for
|
|
81
|
+
// growing intervals before re-issuing the query so a short outage
|
|
82
|
+
// self-heals instead of burning all attempts inside a few seconds.
|
|
83
|
+
await (0, sleep_1.sleep)(attempt * 5000);
|
|
84
|
+
const searchText = await this.searchForConnectorTextbox.inputValue();
|
|
85
|
+
await this.searchForConnectorTextbox.clear();
|
|
86
|
+
await (0, sleep_1.sleep)(2000);
|
|
87
|
+
await this.searchForConnectorTextbox.fill(searchText);
|
|
88
|
+
await (0, sleep_1.sleep)(2000);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error('Marketplace connector search failed after retries due to persistent error or no matching results');
|
|
92
|
+
}
|
|
93
|
+
async downloadConnectorToProject() {
|
|
94
|
+
if (await this.browseAllConnectorMarketplacePage.isOpen()) {
|
|
95
|
+
await this.browseAllConnectorMarketplacePage.importConnectorToProject(this.connectorName);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await this.clickDownloadToProjectButton();
|
|
99
|
+
if (await this.confirmImport()) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// The modal is up but rendered none of the known import actions. Dismiss it
|
|
103
|
+
// so it does not block the next click on the diagram, then fail here:
|
|
104
|
+
// continuing would leave the diagram's connector template unresolved
|
|
105
|
+
// ("Template: Not found") and only surface 60s later as a missing
|
|
106
|
+
// properties-panel group, pointing at the wrong step.
|
|
107
|
+
if (await this.cancelButton.isVisible().catch(() => false)) {
|
|
108
|
+
await this.cancelButton.click({ timeout: 20000 });
|
|
109
|
+
throw new Error('Connector import modal offered no import action ("Add to project", "Replace resource" or "Save as copy"); the import was abandoned.');
|
|
110
|
+
}
|
|
111
|
+
// Hub imported without asking for confirmation, so the write is in flight
|
|
112
|
+
// here too.
|
|
113
|
+
await this.waitForImportToSettle();
|
|
114
|
+
}
|
|
115
|
+
// Hub's import modal labels its primary button after the project state:
|
|
116
|
+
// "Add to project" when the template is new to the project, "Replace
|
|
117
|
+
// resource" when the same template is already there, "Save as copy" on a
|
|
118
|
+
// conflicting version. Probe them in order and click the one actually
|
|
119
|
+
// rendered; clicking "Cancel" instead abandons the import and leaves the
|
|
120
|
+
// diagram's connector template unresolved ("Template: Not found"), which
|
|
121
|
+
// only surfaces much later as a missing properties-panel group.
|
|
122
|
+
async confirmImport() {
|
|
123
|
+
const importButtons = [
|
|
124
|
+
this.addToProjectButton,
|
|
125
|
+
this.replaceResourceButton,
|
|
126
|
+
this.saveAsCopyButton,
|
|
127
|
+
];
|
|
128
|
+
// Carbon animates the modal in, so a click issued mid-transition is
|
|
129
|
+
// rejected ("element is not stable") and by the next Playwright retry the
|
|
130
|
+
// button can already be gone; keep probing the labels until one click
|
|
131
|
+
// actually lands instead of burning the whole budget on a single button.
|
|
132
|
+
const deadline = Date.now() + 45000;
|
|
133
|
+
do {
|
|
134
|
+
for (const button of importButtons) {
|
|
135
|
+
if (!(await button.isVisible().catch(() => false))) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Register the waiter before the click: the write can be acknowledged
|
|
139
|
+
// before the modal finishes unmounting.
|
|
140
|
+
const templateWritten = this.page
|
|
141
|
+
.waitForResponse((response) => response.request().method() === 'POST' &&
|
|
142
|
+
response.url().includes('/internal/files'), { timeout: 30000 })
|
|
143
|
+
.catch(() => null);
|
|
144
|
+
const clicked = await button
|
|
145
|
+
.click({ timeout: 5000 })
|
|
146
|
+
.then(() => true)
|
|
147
|
+
.catch(() => false);
|
|
148
|
+
if (!clicked) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
await (0, test_1.expect)(button).toBeHidden({ timeout: 60000 });
|
|
152
|
+
const written = await templateWritten;
|
|
153
|
+
// A rejected write leaves the project without the template just like
|
|
154
|
+
// an aborted one, so surface the status here instead of letting it
|
|
155
|
+
// resurface 60s later as a missing "Authentication" group in
|
|
156
|
+
// ModelerCreatePage.
|
|
157
|
+
if (written && !written.ok()) {
|
|
158
|
+
throw new Error(`Connector template import failed: writing it into the project returned HTTP ${written.status()}.`);
|
|
159
|
+
}
|
|
160
|
+
await this.waitForImportToSettle();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
await (0, sleep_1.sleep)(1000);
|
|
164
|
+
} while (Date.now() < deadline);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
// Hub unmounts the ImportModal as soon as the click is handled, but writes the
|
|
168
|
+
// template into the project with a POST that is still in flight at that point.
|
|
169
|
+
// Returning right after the button hides let the caller's `page.reload()`
|
|
170
|
+
// abort that write ~20ms later, leaving the project's connector-template list
|
|
171
|
+
// empty and every templated task on the diagram at "Template: Not found" --
|
|
172
|
+
// the properties panel then never renders the "Authentication" group.
|
|
173
|
+
async waitForImportToSettle() {
|
|
174
|
+
await this.page.waitForLoadState('networkidle').catch(() => { });
|
|
175
|
+
}
|
|
176
|
+
getLegacyDownloadButton() {
|
|
177
|
+
if (!this.connectorName) {
|
|
178
|
+
return this.downloadToProjectButton;
|
|
179
|
+
}
|
|
180
|
+
return this.legacyMarketplaceDialog
|
|
181
|
+
.getByText(this.connectorName, { exact: true })
|
|
182
|
+
.first()
|
|
183
|
+
.locator('xpath=ancestor::*[.//button[normalize-space()="Download to project"]][1]')
|
|
184
|
+
.getByRole('button', { name: 'Download to project' });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.ConnectorMarketplacePage = ConnectorMarketplacePage;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
declare class ConnectorSettingsPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly urlInput: Locator;
|
|
5
|
+
readonly resultExpressionInput: Locator;
|
|
6
|
+
readonly authenticationTypeDropdown: Locator;
|
|
7
|
+
readonly usernameInput: Locator;
|
|
8
|
+
readonly passwordInput: Locator;
|
|
9
|
+
readonly methodTypeDropdown: Locator;
|
|
10
|
+
readonly resultVariableInput: Locator;
|
|
11
|
+
readonly bearerTokenInput: Locator;
|
|
12
|
+
readonly authenticationTab: Locator;
|
|
13
|
+
readonly outputMappingTab: Locator;
|
|
14
|
+
readonly httpEndpointTab: Locator;
|
|
15
|
+
readonly correlationTab: Locator;
|
|
16
|
+
readonly inputTab: Locator;
|
|
17
|
+
readonly webhookConfigurationTab: Locator;
|
|
18
|
+
constructor(page: Page);
|
|
19
|
+
clickUrlInput(): Promise<void>;
|
|
20
|
+
fillUrlInput(url: string): Promise<void>;
|
|
21
|
+
clickResultExpressionInput(): Promise<void>;
|
|
22
|
+
fillResultExpressionInput(resultExpression: string): Promise<void>;
|
|
23
|
+
selectAuthenticationType(authentication: string): Promise<void>;
|
|
24
|
+
clickUsernameInput(): Promise<void>;
|
|
25
|
+
fillUsernameInput(username: string): Promise<void>;
|
|
26
|
+
clickPasswordInput(): Promise<void>;
|
|
27
|
+
fillPasswordInput(password: string): Promise<void>;
|
|
28
|
+
selectMethodType(method: string): Promise<void>;
|
|
29
|
+
clickResultVariableInput(): Promise<void>;
|
|
30
|
+
fillResultVariableInput(variable: string): Promise<void>;
|
|
31
|
+
clickBearerTokenInput(): Promise<void>;
|
|
32
|
+
fillBearerTokenInput(token: string): Promise<void>;
|
|
33
|
+
private clickTabWithRetry;
|
|
34
|
+
clickAuthenticationTab(): Promise<void>;
|
|
35
|
+
clickOutputMappingTab(): Promise<void>;
|
|
36
|
+
clickHTTPEndpointTab(): Promise<void>;
|
|
37
|
+
clickCorrelationTab(): Promise<void>;
|
|
38
|
+
clickInputTab(): Promise<void>;
|
|
39
|
+
clickWebhookConfigurationTab(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export { ConnectorSettingsPage };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectorSettingsPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
class ConnectorSettingsPage {
|
|
6
|
+
page;
|
|
7
|
+
urlInput;
|
|
8
|
+
resultExpressionInput;
|
|
9
|
+
authenticationTypeDropdown;
|
|
10
|
+
usernameInput;
|
|
11
|
+
passwordInput;
|
|
12
|
+
methodTypeDropdown;
|
|
13
|
+
resultVariableInput;
|
|
14
|
+
bearerTokenInput;
|
|
15
|
+
authenticationTab;
|
|
16
|
+
outputMappingTab;
|
|
17
|
+
httpEndpointTab;
|
|
18
|
+
correlationTab;
|
|
19
|
+
inputTab;
|
|
20
|
+
webhookConfigurationTab;
|
|
21
|
+
constructor(page) {
|
|
22
|
+
this.page = page;
|
|
23
|
+
this.urlInput = page.getByLabel('URL');
|
|
24
|
+
this.resultExpressionInput = page.getByLabel('Result expression');
|
|
25
|
+
this.authenticationTypeDropdown = page.getByLabel('Type');
|
|
26
|
+
this.usernameInput = page.getByLabel('Username');
|
|
27
|
+
this.passwordInput = page.getByLabel('Password', { exact: true });
|
|
28
|
+
this.methodTypeDropdown = page.getByLabel('Method');
|
|
29
|
+
this.resultVariableInput = page.getByLabel('Result variable');
|
|
30
|
+
this.bearerTokenInput = page.getByLabel('Bearer token');
|
|
31
|
+
this.authenticationTab = page
|
|
32
|
+
.locator('div')
|
|
33
|
+
.filter({ hasText: /^Authentication$/ })
|
|
34
|
+
.first();
|
|
35
|
+
this.outputMappingTab = page
|
|
36
|
+
.locator('div')
|
|
37
|
+
.filter({ hasText: /^Output mapping$/ })
|
|
38
|
+
.first();
|
|
39
|
+
this.httpEndpointTab = page
|
|
40
|
+
.locator('div')
|
|
41
|
+
.filter({ hasText: /^HTTP endpoint$/ })
|
|
42
|
+
.first();
|
|
43
|
+
this.correlationTab = page
|
|
44
|
+
.locator('div')
|
|
45
|
+
.filter({ hasText: /^Correlation$/ })
|
|
46
|
+
.first();
|
|
47
|
+
this.inputTab = page
|
|
48
|
+
.locator('div')
|
|
49
|
+
.filter({ hasText: /^Input$/ })
|
|
50
|
+
.first();
|
|
51
|
+
this.webhookConfigurationTab = page
|
|
52
|
+
.locator('div')
|
|
53
|
+
.filter({ hasText: /^Webhook configuration$/ })
|
|
54
|
+
.first();
|
|
55
|
+
}
|
|
56
|
+
async clickUrlInput() {
|
|
57
|
+
try {
|
|
58
|
+
await this.urlInput.click({ timeout: 60000 });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
await this.httpEndpointTab.click();
|
|
62
|
+
await this.urlInput.click({ timeout: 60000 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async fillUrlInput(url) {
|
|
66
|
+
await this.urlInput.fill(url);
|
|
67
|
+
await (0, test_1.expect)(this.page.getByText('URL must not be empty.')).not.toBeVisible({ timeout: 30000 });
|
|
68
|
+
}
|
|
69
|
+
async clickResultExpressionInput() {
|
|
70
|
+
try {
|
|
71
|
+
await this.resultExpressionInput.click();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
await this.clickOutputMappingTab();
|
|
75
|
+
await this.resultExpressionInput.click();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async fillResultExpressionInput(resultExpression) {
|
|
79
|
+
await this.resultExpressionInput.fill(resultExpression);
|
|
80
|
+
}
|
|
81
|
+
async selectAuthenticationType(authentication) {
|
|
82
|
+
await this.authenticationTypeDropdown.selectOption(authentication);
|
|
83
|
+
}
|
|
84
|
+
async clickUsernameInput() {
|
|
85
|
+
await this.usernameInput.click({ timeout: 30000 });
|
|
86
|
+
}
|
|
87
|
+
async fillUsernameInput(username) {
|
|
88
|
+
await this.usernameInput.fill(username, { timeout: 30000 });
|
|
89
|
+
await (0, test_1.expect)(this.page.getByText('Username must not be empty.')).not.toBeVisible({ timeout: 30000 });
|
|
90
|
+
}
|
|
91
|
+
async clickPasswordInput() {
|
|
92
|
+
await this.passwordInput.click();
|
|
93
|
+
}
|
|
94
|
+
async fillPasswordInput(password) {
|
|
95
|
+
await this.passwordInput.fill(password);
|
|
96
|
+
await (0, test_1.expect)(this.page.getByText('Password must not be empty.')).not.toBeVisible({ timeout: 30000 });
|
|
97
|
+
}
|
|
98
|
+
async selectMethodType(method) {
|
|
99
|
+
await this.methodTypeDropdown.selectOption(method, { timeout: 60000 });
|
|
100
|
+
}
|
|
101
|
+
async clickResultVariableInput() {
|
|
102
|
+
await this.resultVariableInput.click();
|
|
103
|
+
}
|
|
104
|
+
async fillResultVariableInput(variable) {
|
|
105
|
+
await this.resultVariableInput.fill(variable);
|
|
106
|
+
}
|
|
107
|
+
async clickBearerTokenInput() {
|
|
108
|
+
await this.bearerTokenInput.click({ timeout: 30000 });
|
|
109
|
+
}
|
|
110
|
+
async fillBearerTokenInput(token) {
|
|
111
|
+
await this.bearerTokenInput.fill(token);
|
|
112
|
+
await (0, test_1.expect)(this.page.getByText('Bearer token must not be empty.')).not.toBeVisible({ timeout: 30000 });
|
|
113
|
+
}
|
|
114
|
+
// Connector settings tabs can take several seconds to render on slower clusters
|
|
115
|
+
// after the element type is changed. Each clickXxxTab method retries with a
|
|
116
|
+
// reload-free wait so a single slow render doesn't cause a hard timeout failure.
|
|
117
|
+
async clickTabWithRetry(tab, tabName, maxAttempts = 3) {
|
|
118
|
+
let lastError;
|
|
119
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
await (0, test_1.expect)(tab).toBeVisible({ timeout: 30000 });
|
|
122
|
+
await tab.click({ timeout: 30000 });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
lastError = err;
|
|
127
|
+
console.warn(`clickTabWithRetry: "${tabName}" attempt ${attempt + 1}/${maxAttempts} failed: ${err instanceof Error ? err.message : err}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw lastError instanceof Error
|
|
131
|
+
? lastError
|
|
132
|
+
: new Error(`Tab "${tabName}" not clickable after ${maxAttempts} attempts`);
|
|
133
|
+
}
|
|
134
|
+
async clickAuthenticationTab() {
|
|
135
|
+
await this.clickTabWithRetry(this.authenticationTab, 'Authentication');
|
|
136
|
+
}
|
|
137
|
+
async clickOutputMappingTab() {
|
|
138
|
+
await this.clickTabWithRetry(this.outputMappingTab, 'Output mapping');
|
|
139
|
+
}
|
|
140
|
+
async clickHTTPEndpointTab() {
|
|
141
|
+
await this.clickTabWithRetry(this.httpEndpointTab, 'HTTP endpoint');
|
|
142
|
+
}
|
|
143
|
+
async clickCorrelationTab() {
|
|
144
|
+
await this.clickTabWithRetry(this.correlationTab, 'Correlation');
|
|
145
|
+
}
|
|
146
|
+
async clickInputTab() {
|
|
147
|
+
await this.clickTabWithRetry(this.inputTab, 'Input');
|
|
148
|
+
}
|
|
149
|
+
async clickWebhookConfigurationTab() {
|
|
150
|
+
await this.clickTabWithRetry(this.webhookConfigurationTab, 'Webhook configuration');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.ConnectorSettingsPage = ConnectorSettingsPage;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
declare class ConnectorTemplatePage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly publishToOrgButton: Locator;
|
|
5
|
+
readonly publishButton: Locator;
|
|
6
|
+
constructor(page: Page);
|
|
7
|
+
clickPublishToOrgButton(): Promise<void>;
|
|
8
|
+
clickPublishButton(): Promise<void>;
|
|
9
|
+
publishConnector(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export { ConnectorTemplatePage };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectorTemplatePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
class ConnectorTemplatePage {
|
|
6
|
+
page;
|
|
7
|
+
publishToOrgButton;
|
|
8
|
+
publishButton;
|
|
9
|
+
constructor(page) {
|
|
10
|
+
this.page = page;
|
|
11
|
+
this.publishButton = page.getByRole('button', {
|
|
12
|
+
name: 'Publish',
|
|
13
|
+
exact: true,
|
|
14
|
+
});
|
|
15
|
+
this.publishToOrgButton = page.locator('[data-test="publish-to-org"]');
|
|
16
|
+
}
|
|
17
|
+
async clickPublishToOrgButton() {
|
|
18
|
+
await this.publishToOrgButton.click({ timeout: 90000 });
|
|
19
|
+
}
|
|
20
|
+
async clickPublishButton() {
|
|
21
|
+
await this.publishButton.click({ timeout: 90000 });
|
|
22
|
+
}
|
|
23
|
+
async publishConnector() {
|
|
24
|
+
try {
|
|
25
|
+
await (0, test_1.expect)(this.publishToOrgButton).toBeVisible({ timeout: 15000 });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await this.clickPublishToOrgButton();
|
|
31
|
+
await this.clickPublishButton();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.ConnectorTemplatePage = ConnectorTemplatePage;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
declare class FormJsPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly aiFormGeneratorButton: Locator;
|
|
5
|
+
readonly formRequestInput: Locator;
|
|
6
|
+
readonly generateFormButton: Locator;
|
|
7
|
+
readonly formEditor: Locator;
|
|
8
|
+
readonly filePicker: Locator;
|
|
9
|
+
readonly generalPanel: Locator;
|
|
10
|
+
readonly keyInput: Locator;
|
|
11
|
+
readonly uploadMultipleFilesToggle: Locator;
|
|
12
|
+
readonly documentReferenceInput: Locator;
|
|
13
|
+
readonly documentPreview: Locator;
|
|
14
|
+
readonly deployDialog: Locator;
|
|
15
|
+
readonly deployFormButton: Locator;
|
|
16
|
+
readonly deployModalSubmitButton: Locator;
|
|
17
|
+
readonly deploySuccessNotification: Locator;
|
|
18
|
+
constructor(page: Page);
|
|
19
|
+
clickAIFormGeneratorButton(): Promise<void>;
|
|
20
|
+
clickFormRequestInput(): Promise<void>;
|
|
21
|
+
fillFormRequestInput(formRequest: string): Promise<void>;
|
|
22
|
+
clickGenerateFormButton(): Promise<void>;
|
|
23
|
+
dragAndDrop(source: Locator, target: Locator): Promise<void>;
|
|
24
|
+
clickGeneralPropertiesPanel(): Promise<void>;
|
|
25
|
+
fillKeyInput(key: string): Promise<void>;
|
|
26
|
+
enableUploadMultipleFiles(): Promise<void>;
|
|
27
|
+
filldocumentReferenceInput(key: string): Promise<void>;
|
|
28
|
+
clickDeployFormButton(): Promise<void>;
|
|
29
|
+
clickDeployModalSubmitButton(): Promise<void>;
|
|
30
|
+
selectOnlyThisResource(): Promise<void>;
|
|
31
|
+
deployForm(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export { FormJsPage };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FormJsPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
6
|
+
const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
|
|
7
|
+
class FormJsPage {
|
|
8
|
+
page;
|
|
9
|
+
aiFormGeneratorButton;
|
|
10
|
+
formRequestInput;
|
|
11
|
+
generateFormButton;
|
|
12
|
+
formEditor;
|
|
13
|
+
filePicker;
|
|
14
|
+
generalPanel;
|
|
15
|
+
keyInput;
|
|
16
|
+
uploadMultipleFilesToggle;
|
|
17
|
+
documentReferenceInput;
|
|
18
|
+
documentPreview;
|
|
19
|
+
deployDialog;
|
|
20
|
+
deployFormButton;
|
|
21
|
+
deployModalSubmitButton;
|
|
22
|
+
deploySuccessNotification;
|
|
23
|
+
constructor(page) {
|
|
24
|
+
this.page = page;
|
|
25
|
+
this.aiFormGeneratorButton = page.getByRole('button', {
|
|
26
|
+
name: 'AI Form Generator',
|
|
27
|
+
});
|
|
28
|
+
this.formRequestInput = page.getByPlaceholder('A form for an american standard loan request');
|
|
29
|
+
this.generateFormButton = page.getByRole('button', { name: 'Generate form' });
|
|
30
|
+
this.formEditor = page.getByLabel('Form Definition');
|
|
31
|
+
this.filePicker = page.locator('button[data-field-type="filepicker"]');
|
|
32
|
+
this.generalPanel = page.locator('[data-group-id="group-general"]').first();
|
|
33
|
+
this.keyInput = page.getByRole('textbox', { name: 'key' });
|
|
34
|
+
this.uploadMultipleFilesToggle = page.locator('div[data-entry-id="multiple"] .bio-properties-panel-toggle-switch__switcher');
|
|
35
|
+
this.documentReferenceInput = page.getByRole('textbox', {
|
|
36
|
+
name: 'document reference',
|
|
37
|
+
});
|
|
38
|
+
this.documentPreview = page.locator('button[data-field-type="documentPreview"]');
|
|
39
|
+
this.deployDialog = page.getByRole('dialog');
|
|
40
|
+
// The Design System shell renders none of Carbon's cds-- classes, so the
|
|
41
|
+
// editor's Deploy button, the dialog's submit and the success toast are all
|
|
42
|
+
// matched by role and text instead -- which resolves on both shells.
|
|
43
|
+
// .first(): once the deploy dialog is open its submit carries the same
|
|
44
|
+
// accessible name; the editor's button is the earlier one in the DOM.
|
|
45
|
+
this.deployFormButton = page
|
|
46
|
+
.getByRole('button', { name: 'Deploy', exact: true })
|
|
47
|
+
.first();
|
|
48
|
+
this.deployModalSubmitButton = this.deployDialog
|
|
49
|
+
.getByRole('button', { name: 'Deploy', exact: true })
|
|
50
|
+
.or(page.locator('button[data-modal-primary-focus="true"][type="submit"]'))
|
|
51
|
+
.first();
|
|
52
|
+
this.deploySuccessNotification = page.getByText('Form deployed!').first();
|
|
53
|
+
}
|
|
54
|
+
async clickAIFormGeneratorButton() {
|
|
55
|
+
await this.aiFormGeneratorButton.click();
|
|
56
|
+
}
|
|
57
|
+
async clickFormRequestInput() {
|
|
58
|
+
await this.formRequestInput.click();
|
|
59
|
+
}
|
|
60
|
+
async fillFormRequestInput(formRequest) {
|
|
61
|
+
await this.formRequestInput.fill(formRequest);
|
|
62
|
+
}
|
|
63
|
+
async clickGenerateFormButton() {
|
|
64
|
+
await this.generateFormButton.click();
|
|
65
|
+
}
|
|
66
|
+
async dragAndDrop(source, target) {
|
|
67
|
+
await source.waitFor();
|
|
68
|
+
await target.waitFor();
|
|
69
|
+
await source.dragTo(target);
|
|
70
|
+
}
|
|
71
|
+
async clickGeneralPropertiesPanel() {
|
|
72
|
+
await this.generalPanel.click({ timeout: 60000 });
|
|
73
|
+
}
|
|
74
|
+
async fillKeyInput(key) {
|
|
75
|
+
await this.keyInput.fill(key, { timeout: 60000 });
|
|
76
|
+
}
|
|
77
|
+
async enableUploadMultipleFiles() {
|
|
78
|
+
await this.uploadMultipleFilesToggle.click({ timeout: 60000 });
|
|
79
|
+
}
|
|
80
|
+
async filldocumentReferenceInput(key) {
|
|
81
|
+
await this.documentReferenceInput.fill(key, { timeout: 60000 });
|
|
82
|
+
}
|
|
83
|
+
async clickDeployFormButton() {
|
|
84
|
+
await this.deployFormButton.click({ timeout: 60000 });
|
|
85
|
+
}
|
|
86
|
+
async clickDeployModalSubmitButton() {
|
|
87
|
+
await this.deployModalSubmitButton.click({ timeout: 60000 });
|
|
88
|
+
}
|
|
89
|
+
// Since camunda/camunda-hub#25831 a form lives inside a process application,
|
|
90
|
+
// and Hub scopes Deploy to that application unless the scope is narrowed --
|
|
91
|
+
// deployProcessApplication, not deployFile, so the "Form deployed!" toast
|
|
92
|
+
// never fires. Narrowing the scope keeps this a form-deploy test instead of
|
|
93
|
+
// silently turning it into a process-application deploy. Since
|
|
94
|
+
// camunda/camunda-hub#29791 Hub preselects "Only this resource" when
|
|
95
|
+
// deploying from a file, so the shared helper's already-selected guard is
|
|
96
|
+
// what keeps this from reopening the preselected scope select and failing.
|
|
97
|
+
async selectOnlyThisResource() {
|
|
98
|
+
await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.deployDialog);
|
|
99
|
+
}
|
|
100
|
+
async deployForm() {
|
|
101
|
+
await this.clickDeployFormButton();
|
|
102
|
+
await (0, test_1.expect)(this.deployDialog).toBeVisible({ timeout: 30000 });
|
|
103
|
+
await (0, modelerCluster_1.selectModelerCluster)(this.deployDialog);
|
|
104
|
+
await this.selectOnlyThisResource();
|
|
105
|
+
await this.clickDeployModalSubmitButton();
|
|
106
|
+
await (0, test_1.expect)(this.deploySuccessNotification).toBeVisible({ timeout: 30000 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.FormJsPage = FormJsPage;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
declare class HomePage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly dashboardTab: Locator;
|
|
5
|
+
readonly clusterTab: Locator;
|
|
6
|
+
readonly organizationTab: Locator;
|
|
7
|
+
readonly organizationViewTitle: Locator;
|
|
8
|
+
readonly welcomeMessage: Locator;
|
|
9
|
+
readonly informationDialog: Locator;
|
|
10
|
+
readonly camundaAppsButton: Locator;
|
|
11
|
+
readonly camundaComponentsButton: Locator;
|
|
12
|
+
constructor(page: Page);
|
|
13
|
+
clickClusters(): Promise<void>;
|
|
14
|
+
clickDashboard(): Promise<void>;
|
|
15
|
+
clickOrganization(): Promise<void>;
|
|
16
|
+
closeInformationDialog(): Promise<void>;
|
|
17
|
+
clickCamundaComponents(): Promise<void>;
|
|
18
|
+
clickCamundaApps(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export { HomePage };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HomePage = void 0;
|
|
4
|
+
class HomePage {
|
|
5
|
+
page;
|
|
6
|
+
dashboardTab;
|
|
7
|
+
clusterTab;
|
|
8
|
+
organizationTab;
|
|
9
|
+
organizationViewTitle;
|
|
10
|
+
welcomeMessage;
|
|
11
|
+
informationDialog;
|
|
12
|
+
camundaAppsButton;
|
|
13
|
+
camundaComponentsButton;
|
|
14
|
+
constructor(page) {
|
|
15
|
+
this.page = page;
|
|
16
|
+
this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
|
|
17
|
+
this.clusterTab = page.getByRole('link', { name: 'Clusters' });
|
|
18
|
+
this.organizationTab = page.getByRole('link', { name: 'Organization' });
|
|
19
|
+
this.organizationViewTitle = page.getByText('Organization Management');
|
|
20
|
+
this.welcomeMessage = page.getByText('Welcome, demo').first();
|
|
21
|
+
this.informationDialog = page.getByRole('button', {
|
|
22
|
+
name: 'Close this dialog',
|
|
23
|
+
});
|
|
24
|
+
this.camundaComponentsButton = page.getByLabel('Camunda components');
|
|
25
|
+
this.camundaAppsButton = page.getByLabel('Camunda apps');
|
|
26
|
+
}
|
|
27
|
+
async clickClusters() {
|
|
28
|
+
await this.clusterTab.click({ timeout: 90000 });
|
|
29
|
+
}
|
|
30
|
+
async clickDashboard() {
|
|
31
|
+
await this.dashboardTab.click({ timeout: 90000 });
|
|
32
|
+
}
|
|
33
|
+
async clickOrganization() {
|
|
34
|
+
await this.organizationTab.click({ timeout: 90000 });
|
|
35
|
+
}
|
|
36
|
+
async closeInformationDialog() {
|
|
37
|
+
const isLocalTest = process.env.LOCAL_TEST;
|
|
38
|
+
if (isLocalTest) {
|
|
39
|
+
await this.informationDialog.click();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async clickCamundaComponents() {
|
|
43
|
+
await this.camundaComponentsButton.click({ timeout: 90000 });
|
|
44
|
+
}
|
|
45
|
+
async clickCamundaApps() {
|
|
46
|
+
await this.camundaAppsButton.click({ timeout: 90000 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.HomePage = HomePage;
|