graphdb-workbench-tests 2.0.0-TR7 → 2.0.0
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/fixtures/locale-en.json +1608 -0
- package/integration/explore/similarity.spec.js +3 -3
- package/integration/explore/visual.graph.spec.js +46 -12
- package/integration/help/rest-api.spec.js +1 -1
- package/integration/home/language-change.spec.js +45 -0
- package/integration/home/workbench.home.spec.js +16 -1
- package/integration/repository/repositories.spec.js +54 -29
- package/integration/setup/connectors-lucene.spec.js +35 -30
- package/integration/setup/my-settings.spec.js +17 -0
- package/integration/setup/plugins.spec.js +69 -0
- package/integration/setup/rdf-rank.spec.js +1 -1
- package/integration/setup/sparql-templates.spec.js +179 -0
- package/integration/setup/user-and-access.spec.js +69 -24
- package/integration/sparql/main.menu.spec.js +8 -13
- package/integration/sparql/sparql-language-change.spec.js +62 -0
- package/integration/sparql/sparql.menu.spec.js +105 -203
- package/package.json +4 -2
- package/plugins/index.js +9 -0
- package/steps/home-steps.js +16 -0
- package/steps/sparql-steps.js +154 -0
- package/support/import-commands.js +8 -6
- package/support/index.js +2 -0
- package/support/settings-commands.js +1 -1
- package/support/sparql-commands.js +3 -5
- package/integration/import/onto-refine.spec.js +0 -135
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
describe('SPARQL Templates', () => {
|
|
2
|
+
|
|
3
|
+
let repositoryId;
|
|
4
|
+
const TEMPLATE_NAME = "http://example.com/salary_template";
|
|
5
|
+
const SPARQL_TEMPLATE = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n" +
|
|
6
|
+
"PREFIX factory: <http://factory/>\n" +
|
|
7
|
+
"PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n" +
|
|
8
|
+
"PREFIX spif: <http://spinrdf.org/spif#>\n" +
|
|
9
|
+
"INSERT {\n" +
|
|
10
|
+
"?id <http://factory/updatedOn> \"2021-10-01T07:55:38.238Z\"^^xsd:dateTime .\n" +
|
|
11
|
+
"?split spif:split (\"blaaa/blaa\" \"/\")\n" +
|
|
12
|
+
"} WHERE {\n" +
|
|
13
|
+
"?id rdf:type <http://factory/Factory> .\n" +
|
|
14
|
+
"?worker <http://factory/worksIn> ?id .\n" +
|
|
15
|
+
"?worker <http://factory/hasSalary> ?oldSalary .\n" +
|
|
16
|
+
"FILTER regex(STR(?id), \"fac\", \"i\")\n" +
|
|
17
|
+
"?split spif:split (\"blaaa/blaa\" \"/\")\n" +
|
|
18
|
+
"bind(now() as ?now)\n" +
|
|
19
|
+
"}";
|
|
20
|
+
const DEFAULT_QUERY = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n" +
|
|
21
|
+
"PREFIX ex: <http://example.com#>\n" +
|
|
22
|
+
"DELETE {\n" +
|
|
23
|
+
" ?subject ex:myPredicate ?oldValue .\n" +
|
|
24
|
+
"} INSERT {\n" +
|
|
25
|
+
" ?subject ex:myPredicate ?newValue .\n" +
|
|
26
|
+
"} WHERE {\n" +
|
|
27
|
+
" ?id rdf:type ex:MyType .\n" +
|
|
28
|
+
" ?subject ex:isRelatedTo ?id .\n" +
|
|
29
|
+
"}\n";
|
|
30
|
+
|
|
31
|
+
before(() => {
|
|
32
|
+
repositoryId = 'sparql-templates-repo' + Date.now();
|
|
33
|
+
cy.createRepository({id: repositoryId});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
cy.visit('/sparql-templates', {
|
|
38
|
+
onBeforeLoad: (win) => {
|
|
39
|
+
win.localStorage.setItem('com.ontotext.graphdb.repository', repositoryId);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
cy.window()
|
|
43
|
+
.then(() => getTemplatesTable().scrollIntoView().should('be.visible'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
after(() => {
|
|
47
|
+
cy.deleteRepository(repositoryId);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('Initial state', () => {
|
|
51
|
+
cy.url().should('include', '/sparql-templates');
|
|
52
|
+
//Verify templates table is empty
|
|
53
|
+
getTemplatesTable().should('be.visible')
|
|
54
|
+
.and('contain','No templates are defined');
|
|
55
|
+
//Verify create template button is visible
|
|
56
|
+
getCreateSparqlTemplateButton().should('be.visible');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('Should create/edit/delete a SPARQL template', () => {
|
|
60
|
+
//Click create template button
|
|
61
|
+
getCreateSparqlTemplateButton().click();
|
|
62
|
+
cy.waitUntilQueryIsVisible();
|
|
63
|
+
//Test Template IRI field validation
|
|
64
|
+
getSaveButton().click();
|
|
65
|
+
//Verify error toast
|
|
66
|
+
getToastError()
|
|
67
|
+
.should('be.visible')
|
|
68
|
+
.and('contain','SPARQL template IRI is required');
|
|
69
|
+
//Type a valid Template IRI value in the filed
|
|
70
|
+
getTemplateNameField().type(TEMPLATE_NAME);
|
|
71
|
+
//Verify default query
|
|
72
|
+
verifyQueryAreaEquals(DEFAULT_QUERY);
|
|
73
|
+
clearQuery();
|
|
74
|
+
//Paste new template query and verify content
|
|
75
|
+
cy.pasteQuery(SPARQL_TEMPLATE);
|
|
76
|
+
verifyQueryAreaEquals(SPARQL_TEMPLATE);
|
|
77
|
+
getSaveButton()
|
|
78
|
+
.scrollIntoView()
|
|
79
|
+
.click()
|
|
80
|
+
.then(() => {
|
|
81
|
+
cy.waitUntil(() =>
|
|
82
|
+
cy.get('.edit-query-btn')
|
|
83
|
+
.then(editBtn => editBtn));
|
|
84
|
+
});
|
|
85
|
+
//Verify new template is stored in the templates table
|
|
86
|
+
getTemplatesTable().should('be.visible')
|
|
87
|
+
.and('contain',TEMPLATE_NAME);
|
|
88
|
+
//Edit template
|
|
89
|
+
getEditTemplateButton(TEMPLATE_NAME);
|
|
90
|
+
cy.waitUntilQueryIsVisible();
|
|
91
|
+
verifyQueryAreaEquals(SPARQL_TEMPLATE);
|
|
92
|
+
clearQuery();
|
|
93
|
+
//Change query to the default template again
|
|
94
|
+
cy.pasteQuery(DEFAULT_QUERY);
|
|
95
|
+
verifyQueryAreaEquals(DEFAULT_QUERY);
|
|
96
|
+
getSaveButton().click();
|
|
97
|
+
//Verify change to default template is persisted
|
|
98
|
+
getEditTemplateButton(TEMPLATE_NAME);
|
|
99
|
+
cy.waitUntilQueryIsVisible();
|
|
100
|
+
verifyQueryAreaEquals(DEFAULT_QUERY);
|
|
101
|
+
//Cancel as no changes have been made
|
|
102
|
+
getCancelButton().click();
|
|
103
|
+
//Delete template and verify templates table is empty
|
|
104
|
+
getDeleteTemplateButton(TEMPLATE_NAME);
|
|
105
|
+
getConfirmDeleteTemplateButton().click();
|
|
106
|
+
cy.url().should('include', '/sparql-templates');
|
|
107
|
+
getTemplatesTable().should('be.visible')
|
|
108
|
+
.and('contain','No templates are defined');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function getTemplatesTable() {
|
|
112
|
+
return cy.get('.sparql-templates-list');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getCreateSparqlTemplateButton() {
|
|
116
|
+
return cy.get('.clearfix .create-sql-table-configuration');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getTemplateNameField() {
|
|
120
|
+
return cy.get('.sparql-template-name');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function verifyQueryAreaEquals(query) {
|
|
124
|
+
// Using the CodeMirror instance because getting the value from the DOM is very cumbersome
|
|
125
|
+
getQueryArea().should(codeMirrorEl => {
|
|
126
|
+
const cm = codeMirrorEl[0].CodeMirror;
|
|
127
|
+
expect(cm.getValue().trim()).to.equal(query.trim());
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getQueryArea() {
|
|
132
|
+
return cy.get('#queryEditor .CodeMirror');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clearQuery() {
|
|
136
|
+
// Using force because the textarea is not visible
|
|
137
|
+
getQueryTextArea().type(Cypress.env('modifierKey') + 'a{backspace}', {force: true});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getQueryTextArea() {
|
|
141
|
+
return getQueryArea().find('textarea');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getSaveButton() {
|
|
145
|
+
return cy.get('.save-query-btn');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getCancelButton() {
|
|
149
|
+
return cy.get('.cancel-query-btn');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getToastError() {
|
|
153
|
+
return cy.get('#toast-container');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getEditTemplateButton(templateName) {
|
|
157
|
+
return cy.get('#configurations-table tr')
|
|
158
|
+
.contains(templateName)
|
|
159
|
+
.parent()
|
|
160
|
+
.parent()
|
|
161
|
+
.within(() => {
|
|
162
|
+
cy.get('.icon-edit').click();
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getDeleteTemplateButton(templateName) {
|
|
167
|
+
return cy.get('#configurations-table tr')
|
|
168
|
+
.contains(templateName)
|
|
169
|
+
.parent()
|
|
170
|
+
.parent()
|
|
171
|
+
.within(() => {
|
|
172
|
+
cy.get('.icon-trash').click();
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getConfirmDeleteTemplateButton() {
|
|
177
|
+
return cy.get('.confirm-btn');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
@@ -22,6 +22,17 @@ describe('User and Access', () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
after(() => {
|
|
25
|
+
cy.visit('/users');
|
|
26
|
+
getUsersTable().should('be.visible');
|
|
27
|
+
cy.get('#wb-users-userInUsers tr').then((table) => {
|
|
28
|
+
cy.get('table > tbody > tr').each(($el, index, $list) => {
|
|
29
|
+
getUsersTable().should('be.visible');
|
|
30
|
+
const username = $el.find('.username').text();
|
|
31
|
+
if (username !=='admin') {
|
|
32
|
+
deleteUser(username);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
25
36
|
cy.deleteRepository(repositoryId);
|
|
26
37
|
});
|
|
27
38
|
|
|
@@ -44,7 +55,7 @@ describe('User and Access', () => {
|
|
|
44
55
|
cy.get('@user').find('.edit-user-btn').should('be.visible')
|
|
45
56
|
.and('not.be.disabled');
|
|
46
57
|
// And cannot be deleted
|
|
47
|
-
cy.get('@user').find('.delete-user-btn').should('not.
|
|
58
|
+
cy.get('@user').find('.delete-user-btn').should('not.exist');
|
|
48
59
|
// Date created should be visible
|
|
49
60
|
cy.get('@user').find('.date-created').should('be.visible');
|
|
50
61
|
});
|
|
@@ -80,22 +91,33 @@ describe('User and Access', () => {
|
|
|
80
91
|
cy.get('.ot-splash').should('not.be.visible');
|
|
81
92
|
getUsersTable().should('be.visible');
|
|
82
93
|
//delete repository manager
|
|
83
|
-
deleteUser("repo-manager")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
deleteUser("repo-manager")
|
|
95
|
+
.then(() => {
|
|
96
|
+
//create a custom admin
|
|
97
|
+
createUser("second-admin", PASSWORD, ROLE_CUSTOM_ADMIN);
|
|
98
|
+
logout();
|
|
99
|
+
//login with custom admin
|
|
100
|
+
loginWithUser("second-admin", PASSWORD);
|
|
101
|
+
cy.url().should('include', '/users');
|
|
102
|
+
logout();
|
|
103
|
+
//login with admin
|
|
104
|
+
loginWithUser("admin", DEFAULT_ADMIN_PASSWORD);
|
|
105
|
+
cy.get('.ot-splash').should('not.be.visible');
|
|
106
|
+
getUsersTable().should('be.visible');
|
|
107
|
+
//delete custom admin
|
|
108
|
+
deleteUser("second-admin")
|
|
109
|
+
.then(() => {
|
|
110
|
+
//disable security
|
|
111
|
+
getToggleSecuritySwitch().click();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it('Warn users when setting no password when creating new user admin', () => {
|
|
94
116
|
getUsersTable().should('be.visible');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
createUser("adminWithNoPassword", PASSWORD, ROLE_CUSTOM_ADMIN);
|
|
118
|
+
getUsersTable().should('be.visible');
|
|
119
|
+
cy.get('.ot-splash').should('not.be.visible');
|
|
120
|
+
deleteUser("adminWithNoPassword");
|
|
99
121
|
});
|
|
100
122
|
|
|
101
123
|
function getCreateNewUserButton() {
|
|
@@ -145,25 +167,48 @@ describe('User and Access', () => {
|
|
|
145
167
|
getPasswordField().type(password);
|
|
146
168
|
getConfirmPasswordField().type(password);
|
|
147
169
|
getRoleRadioButton(role).click();
|
|
148
|
-
if(role === "#roleUser") {
|
|
170
|
+
if (role === "#roleUser") {
|
|
149
171
|
getRepoitoryRightsList().contains('Any data repository').nextUntil('.write').within(() => {
|
|
150
172
|
cy.get('.write').click();
|
|
151
173
|
});
|
|
174
|
+
getConfirmUserCreateButton().click();
|
|
175
|
+
} else if (role === "#roleAdmin" && username === "adminWithNoPassword") {
|
|
176
|
+
cy.get('#noPassword:checkbox').check()
|
|
177
|
+
.then(() => {
|
|
178
|
+
cy.get('#noPassword:checkbox')
|
|
179
|
+
.should('be.checked');
|
|
180
|
+
});
|
|
181
|
+
getConfirmUserCreateButton().click()
|
|
182
|
+
.then(() => {
|
|
183
|
+
cy.get('.modal-dialog').find('.lead').contains('If the password is unset and security is enabled, this administrator will not be ' +
|
|
184
|
+
'able to log into GraphDB through the workbench. Are you sure that you want to continue?');
|
|
185
|
+
cy.get('.modal-dialog').find('.confirm-btn').click();
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
getConfirmUserCreateButton().click();
|
|
152
189
|
}
|
|
153
|
-
getConfirmUserCreateButton().click();
|
|
154
190
|
cy.get('.ot-splash').should('not.be.visible');
|
|
155
|
-
getUsersTable().should('contain',username);
|
|
191
|
+
getUsersTable().should('contain', username);
|
|
156
192
|
}
|
|
157
193
|
|
|
158
194
|
function deleteUser(username) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
findUserInTable(username);
|
|
196
|
+
cy.get('@user')
|
|
197
|
+
.should('have.length', 1)
|
|
198
|
+
.within(() => {
|
|
199
|
+
cy.get('.delete-user-btn')
|
|
200
|
+
.as('deleteBtn');
|
|
201
|
+
});
|
|
202
|
+
return cy.waitUntil(() =>
|
|
203
|
+
cy.get('@deleteBtn')
|
|
204
|
+
.then(deleteBtn => deleteBtn && Cypress.dom.isAttached(deleteBtn) && deleteBtn.trigger('click')))
|
|
205
|
+
.then(() => {
|
|
206
|
+
cy.get('.confirm-btn').click();
|
|
207
|
+
});
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
function loginWithUser(username, password) {
|
|
166
|
-
cy.get('#wb-login-username').type(username)
|
|
211
|
+
cy.get('#wb-login-username').type(username);
|
|
167
212
|
cy.get('#wb-login-password').type(password);
|
|
168
213
|
cy.get('#wb-login-submitLogin').click();
|
|
169
214
|
}
|
|
@@ -16,18 +16,7 @@ describe('Main menu tests', function () {
|
|
|
16
16
|
{
|
|
17
17
|
name: 'Import',
|
|
18
18
|
visible: true,
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
name: 'RDF',
|
|
22
|
-
visible: false,
|
|
23
|
-
redirect: '/import'
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: 'Tabular (OntoRefine)',
|
|
27
|
-
visible: false,
|
|
28
|
-
redirect: '/ontorefine'
|
|
29
|
-
}
|
|
30
|
-
]
|
|
19
|
+
redirect: '/import'
|
|
31
20
|
},
|
|
32
21
|
{
|
|
33
22
|
name: 'Explore',
|
|
@@ -110,6 +99,11 @@ describe('Main menu tests', function () {
|
|
|
110
99
|
visible: false,
|
|
111
100
|
redirect: '/cluster'
|
|
112
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Plugins',
|
|
104
|
+
visible: false,
|
|
105
|
+
redirect: '/plugins'
|
|
106
|
+
},
|
|
113
107
|
{
|
|
114
108
|
name: 'Namespaces',
|
|
115
109
|
visible: false,
|
|
@@ -164,7 +158,8 @@ describe('Main menu tests', function () {
|
|
|
164
158
|
let menuVisibilityCheck = menu.visible ? 'be.visible' : 'not.be.visible';
|
|
165
159
|
// Verify that main menu items are present and their visibility
|
|
166
160
|
cy.get('.main-menu .menu-element-root').eq(menuIndex + 1).as('menu')
|
|
167
|
-
.should(menuVisibilityCheck).
|
|
161
|
+
.should(menuVisibilityCheck).within(() =>
|
|
162
|
+
cy.get('.menu-item').contains(menu.name));
|
|
168
163
|
|
|
169
164
|
// Verify submenu items and their visibility when the main menu is not opened
|
|
170
165
|
(menu.submenu || []).forEach((submenu, submenuIndex) => {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import SparqlSteps from '../../steps/sparql-steps';
|
|
2
|
+
|
|
3
|
+
describe('YASQE and YASR language change validation', () => {
|
|
4
|
+
let repositoryId;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
repositoryId = 'sparql-' + Date.now();
|
|
8
|
+
SparqlSteps.createRepoAndVisit(repositoryId);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// Change the language back to English
|
|
13
|
+
SparqlSteps.changeLanguage('en');
|
|
14
|
+
|
|
15
|
+
cy.deleteRepository(repositoryId);
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
context('Default language should be active and language change should affect labels', () => {
|
|
19
|
+
it('should change labels in SPARQL view', () => {
|
|
20
|
+
|
|
21
|
+
// Check some labels are in default language
|
|
22
|
+
SparqlSteps.getSparqlQueryUpdateLabel().should('contain', 'SPARQL Query & Update');
|
|
23
|
+
SparqlSteps.getDownloadBtn().should('contain', 'Download as');
|
|
24
|
+
SparqlSteps.getEditorAndResultsBtn().should('contain', 'Editor and results');
|
|
25
|
+
SparqlSteps.getResultsOnlyBtn().should('contain', 'Results only');
|
|
26
|
+
|
|
27
|
+
SparqlSteps.changeLanguage('fr');
|
|
28
|
+
|
|
29
|
+
// The text in the labels should change
|
|
30
|
+
SparqlSteps.getSparqlQueryUpdateLabel().should('contain', 'Requête et mise à jour SPARQL');
|
|
31
|
+
SparqlSteps.getDownloadBtn().should('contain', 'Téléchargement');
|
|
32
|
+
SparqlSteps.getEditorAndResultsBtn().should('contain', 'Éditeur et résultats');
|
|
33
|
+
SparqlSteps.getResultsOnlyBtn().should('contain', 'Résultats seulement');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should change labels in SPARQL results view', function () {
|
|
37
|
+
SparqlSteps.selectSavedQuery('Add statements');
|
|
38
|
+
SparqlSteps.executeQuery();
|
|
39
|
+
SparqlSteps.selectSavedQuery('SPARQL Select template');
|
|
40
|
+
SparqlSteps.executeQuery();
|
|
41
|
+
|
|
42
|
+
// Go to Results only view
|
|
43
|
+
SparqlSteps.getResultsOnlyBtn().click();
|
|
44
|
+
|
|
45
|
+
// Check some labels are in default language
|
|
46
|
+
SparqlSteps.getTabWithTableText().should('contain', 'Table');
|
|
47
|
+
SparqlSteps.getTabWithRawResponseText().should('contain', 'Raw Response');
|
|
48
|
+
SparqlSteps.getTabWithPivotTableText().should('contain', 'Pivot Table');
|
|
49
|
+
SparqlSteps.getTabWithGoogleChartText().should('contain', 'Google Chart');
|
|
50
|
+
SparqlSteps.getResultsDescription().should('contain', 'Showing results from');
|
|
51
|
+
|
|
52
|
+
SparqlSteps.changeLanguage('fr');
|
|
53
|
+
|
|
54
|
+
// The text in the labels should change
|
|
55
|
+
SparqlSteps.getTabWithTableText().should('contain', 'Tableau');
|
|
56
|
+
SparqlSteps.getTabWithRawResponseText().should('contain', 'Réponse brute');
|
|
57
|
+
SparqlSteps.getTabWithPivotTableText().should('contain', 'Table de pivotement');
|
|
58
|
+
SparqlSteps.getTabWithGoogleChartText().should('contain', 'Graphique Google');
|
|
59
|
+
SparqlSteps.getResultsDescription().should('contain', 'Liste de résultats de');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
})
|