Commit e6769e0c authored by Yadd's avatar Yadd

Merge branch 'master' into v2.0

parents 94679c3d 504e892e
...@@ -130,7 +130,7 @@ Copyright: 2009-2013 Jeff Mott ...@@ -130,7 +130,7 @@ Copyright: 2009-2013 Jeff Mott
License: Expat License: Expat
Files: lemonldap-ng-manager/site/htdocs/static/bwr/es5-shim/* Files: lemonldap-ng-manager/site/htdocs/static/bwr/es5-shim/*
Copyright: 2009-2015, Kristopher Michael Kowal and contributors Copyright: 2009-2015, contributors
License: Expat License: Expat
Files: lemonldap-ng-manager/site/htdocs/static/bwr/file-saver.js/* Files: lemonldap-ng-manager/site/htdocs/static/bwr/file-saver.js/*
......
...@@ -120,6 +120,7 @@ VHOSTLISTEN="*:$(PORT)" ...@@ -120,6 +120,7 @@ VHOSTLISTEN="*:$(PORT)"
TESTWEBSERVER=apache TESTWEBSERVER=apache
TESTWEBSERVERPORT=19876 TESTWEBSERVERPORT=19876
TESTUSESSL=0 TESTUSESSL=0
E2E_TESTS='portal/*.js'
# LDAP backend test # LDAP backend test
LLNGTESTLDAP_SLAPD_BIN=/usr/sbin/slapd LLNGTESTLDAP_SLAPD_BIN=/usr/sbin/slapd
...@@ -548,7 +549,7 @@ launch_protractor: ...@@ -548,7 +549,7 @@ launch_protractor:
# Start e2e tests # Start e2e tests
# NB: you must have protractor installed (using npm install -g protractor) # NB: you must have protractor installed (using npm install -g protractor)
# and have run update-webdriver at least once and have a node.js > 4.0 # and have run update-webdriver at least once and have a node.js > 4.0
@TESTWEBSERVERPORT=$(TESTWEBSERVERPORT) protractor e2e-tests/protractor-conf.js @E2E_TESTS=$(E2E_TESTS) TESTWEBSERVERPORT=$(TESTWEBSERVERPORT) protractor e2e-tests/protractor-conf.js
stop_web_server: stop_web_server:
# Stop web server # Stop web server
...@@ -568,7 +569,7 @@ plackup: ...@@ -568,7 +569,7 @@ plackup:
-F >e2e-tests/conf/fastcgi.log 2>&1 & -F >e2e-tests/conf/fastcgi.log 2>&1 &
install_test: install_test:
@TESTWEBSERVERPORT=$(PORT) protractor e2e-tests/protractor-conf.js @E2E_TESTS=$(E2E_TESTS) TESTWEBSERVERPORT=$(PORT) protractor e2e-tests/protractor-conf.js
# Install targets # Install targets
# --------------- # ---------------
......
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
"description" : "Official LemonLDAP::NG Website", "description" : "Official LemonLDAP::NG Website",
"display" : "on", "display" : "on",
"logo" : "network.png", "logo" : "network.png",
"name" : "Offical Website", "name" : "Official Website",
"uri" : "http://lemonldap-ng.org/" "uri" : "http://lemonldap-ng.org/"
}, },
"type" : "application" "type" : "application"
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
"authentication" : "Demo", "authentication" : "Demo",
"cfgAuthor" : "The LemonLDAP::NG team", "cfgAuthor" : "The LemonLDAP::NG team",
"cfgNum" : 1, "cfgNum" : 1,
"cfgVersion" : "2.0.0", "cfgVersion" : "2.0.1",
"cookieName" : "lemonldap", "cookieName" : "lemonldap",
"demoExportedVars" : { "demoExportedVars" : {
"cn" : "cn", "cn" : "cn",
......
lemonldap-ng (2.1.0) artful; urgency=medium
lemonldap-ng (2.0.1) artful; urgency=medium lemonldap-ng (2.0.1) artful; urgency=medium
* Bugs: * Bugs:
......
...@@ -277,6 +277,7 @@ Suggests: libcrypt-u2f-server-perl, ...@@ -277,6 +277,7 @@ Suggests: libcrypt-u2f-server-perl,
libglib-perl, libglib-perl,
libgssapi-perl, libgssapi-perl,
libimage-magick-perl, libimage-magick-perl,
libipc-run-perl,
liblasso-perl, liblasso-perl,
libnet-facebook-oauth2-perl (>= 0.10), libnet-facebook-oauth2-perl (>= 0.10),
libnet-openid-consumer-perl, libnet-openid-consumer-perl,
......
...@@ -130,7 +130,7 @@ Copyright: 2009-2013 Jeff Mott ...@@ -130,7 +130,7 @@ Copyright: 2009-2013 Jeff Mott
License: Expat License: Expat
Files: lemonldap-ng-manager/site/htdocs/static/bwr/es5-shim/* Files: lemonldap-ng-manager/site/htdocs/static/bwr/es5-shim/*
Copyright: 2009-2015, Kristopher Michael Kowal and contributors Copyright: 2009-2015, contributors
License: Expat License: Expat
Files: lemonldap-ng-manager/site/htdocs/static/bwr/file-saver.js/* Files: lemonldap-ng-manager/site/htdocs/static/bwr/file-saver.js/*
......
#End 2 End Testing (Protractor)
To run the end-2-end tests against the application you use [Protractor](https://github.com/angular/protractor).
## Testing with Protractor
As a one-time setup, download webdriver.
```
npm run update-webdriver
```
Start the Protractor test runner using the e2e configuration:
```
make e2e_test
```
## Devel tips
{
locator_: {
using: 'css selector',
value: '[ng-click="getLanguage(lang)"]'
},
parentElementFinder_: null,
opt_actionResult_: {
then: [Function: then],
cancel: [Function: cancel],
isPending: [Function: isPending]
},
opt_index_: 1,
click: [Function],
sendKeys: [Function],
getTagName: [Function],
getCssValue: [Function],
getAttribute: [Function],
getText: [Function],
getSize: [Function],
getLocation: [Function],
isEnabled: [Function],
isSelected: [Function],
submit: [Function],
clear: [Function],
isDisplayed: [Function],
getOuterHtml: [Function],
getInnerHtml: [Function],
toWireValue: [Function]
}
...@@ -57,6 +57,16 @@ ...@@ -57,6 +57,16 @@
}, },
"type": "application" "type": "application"
}, },
"0008-app": {
"options": {
"description": "Explore WebSSO 2FA sessions",
"display": "auto",
"logo": "database.png",
"name": "2FA Sessions explorer",
"uri": "http://manager.example.com:__port__/2ndfa.html"
},
"type": "application"
},
"type": "category" "type": "category"
}, },
"0008-cat": { "0008-cat": {
...@@ -76,7 +86,7 @@ ...@@ -76,7 +86,7 @@
"description": "Official LemonLDAP::NG Website", "description": "Official LemonLDAP::NG Website",
"display": "on", "display": "on",
"logo": "network.png", "logo": "network.png",
"name": "Offical Website", "name": "Official Website",
"uri": "http://lemonldap-ng.org/" "uri": "http://lemonldap-ng.org/"
}, },
"type": "application" "type": "application"
......
...@@ -3,14 +3,13 @@ ...@@ -3,14 +3,13 @@
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
describe('00 Lemonldap::NG', function() { describe('00 Lemonldap::NG', function() {
describe('Auth mechanism', function() { describe('Auth mechanism', function() {
it('should want to authenticate', function() { it('should want to authenticate', function() {
browser.driver.get('http://auth.example.com:' + process.env.TESTWEBSERVERPORT + '/'); browser.driver.get('http://auth.example.com:' + process.env.TESTWEBSERVERPORT + '/');
// Login attempt
browser.driver.findElement(by.xpath("//input[@name='user']")).sendKeys('dwho'); browser.driver.findElement(by.xpath("//input[@name='user']")).sendKeys('dwho');
browser.driver.findElement(by.xpath("//input[@name='password']")).sendKeys('dwho'); browser.driver.findElement(by.xpath("//input[@name='password']")).sendKeys('dwho');
browser.driver.findElement(by.xpath("//button[@type='submit']")).click(); browser.driver.findElement(by.xpath("//button[@type='submit']")).click();
}); });
}); });
}); });
\ No newline at end of file
'use strict';
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
describe('01 Lemonldap::NG Manager', function() {
describe('Tree display -> General Parameters', function() {
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
// Portal
it('General Parameters > Portal -> Append 4 sub nodes', function() {
element(by.id('a-portalParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(14);
});
it('General Parameters > Portal > URL => Match', function() {
element(by.id('t-portal')).click();
expect(element(by.id('textinput')).getAttribute('value')).toEqual('http://auth.example.com:' + process.env.TESTWEBSERVERPORT + '/');
});
it('General Parameters > Portal > Menu => Append 2 sub nodes', function() {
element(by.id('a-portalMenu')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(16);
});
it('General Parameters > Portal > Menu > Module Activation => Append 5 sub nodes', function() {
element(by.id('a-portalModules')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(21);
});
it('General Parameters > Portal > Menu > Cat. and Apps. => Append 11 sub nodes', function() {
element(by.id('a-applicationList')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(32);
});
it('General Parameters > Portal > Customization => Append 8 sub nodes', function() {
element(by.id('a-portalCustomization')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(40);
});
it('General Parameters > Portal > Customization > Buttons => Append 4 sub nodes', function() {
element(by.id('a-portalButtons')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(44);
});
it('General Parameters > Portal > Customization > Password Management => Append 3 sub nodes', function() {
element(by.id('a-passwordManagement')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(47);
});
it('General Parameters > Portal > Customization > Other => Append 6 sub nodes', function() {
element(by.id('a-portalOther')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(53);
});
// Authentication Parameters
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
it('General Parameters > Authn. parameters => Append 4 sub nodes', function() {
element(by.id('a-authParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(14);
});
it('General Parameters > Authn. parameters > Authn. modules => Should have 26 modules availabled with "Demonstration" selected', function() {
element(by.id('t-authentication')).click();
expect(element(by.css('option[selected="selected"]')).getAttribute('Value')).toEqual('Demo');
expect(element.all(by.repeater('item in currentNode.select')).count()).toEqual(26);
});
it('General Parameters > Authn. parameters > Users modules => Should have 7 modules availabled with "Same" selected', function() {
element(by.id('t-userDB')).click();
expect(element(by.css('option[selected="selected"]')).getAttribute('Value')).toEqual('Same');
expect(element.all(by.repeater('item in currentNode.select')).count()).toEqual(7);
});
it('General Parameters > Authn. parameters > Password modules => Should have 8 modules availabled with "Demo" selected', function() {
element(by.id('t-passwordDB')).click();
expect(element(by.css('option[selected="selected"]')).getAttribute('Value')).toEqual('Demo');
expect(element.all(by.repeater('item in currentNode.select')).count()).toEqual(8);
});
it('General Parameters > Authn. parameters > Register modules => Should have 5 modules availabled with "Demo" selected', function() {
element(by.id('t-registerDB')).click();
expect(element(by.css('option[selected="selected"]')).getAttribute('Value')).toEqual('Demo');
expect(element.all(by.repeater('item in currentNode.select')).count()).toEqual(5);
});
it('should have a hash form if a key is clicked', function() {
element(by.id('a-demoParams')).click();
element(by.id('a-demoExportedVars')).click();
element(by.id('t-demoExportedVars/cn')).click();
expect(element.all(by.id('hashkeyinput')).count()).toEqual(1);
});
// Issuer Modules
it('Main => should display 12 main nodes', function() {
browser.get('/');
//var mainNodes = element.all(by.repeater('node in data track by node.id'));
//expect(mainNodes.count()).toEqual(12);
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
it('General Parameters > Issuer modules => Append 5 sub nodes', function() {
element(by.id('a-issuerParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(15);
});
it('General Parameters > Issuer modules > SAML => Append 3 sub nodes', function() {
element(by.id('a-issuerDBSAML')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(18);
});
it('General Parameters > Issuer modules > CAS => Append 3 sub nodes', function() {
element(by.id('a-issuerDBCAS')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(21);
});
it('General Parameters > Issuer modules > OpenID => Append 4 sub nodes', function() {
element(by.id('a-issuerDBOpenID')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(25);
});
it('General Parameters > Issuer modules > OpenIDConnect => Append 3 sub nodes', function() {
element(by.id('a-issuerDBOpenIDConnect')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(28);
});
it('General Parameters > Issuer modules > GET => Append 4 sub nodes', function() {
element(by.id('a-issuerDBGet')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(32);
});
// Logs
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
it('General Parameters > Logs => Append 3 sub nodes', function() {
element(by.id('a-logParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(13);
});
// Cookies
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
it('General Parameters > Cookies => Append 6 sub nodes', function() {
element(by.id('a-cookieParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(16);
});
// Sessions
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
it('General Parameters > Sessions => Append 8 sub nodes', function() {
element(by.id('a-sessionParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(18);
});
it('General Parameters > Sessions > Opening conditions => New grant rule button + 2 rules', function() {
element(by.id('t-grantSessionRules')).click();
element(by.css('[ng-click="menuClick(button)"]')).click();
element(by.css('[ng-click="menuClick(button)"]')).click();
expect(element.all(by.repeater('s in currentNode.nodes')).count()).toEqual(2);
expect(element.all(by.xpath('//tbody/tr')).count()).toEqual(2);
expect(element.all(by.xpath('//tbody/tr/td/input')).count()).toEqual(6);
});
it('General Parameters > Sessions > Session storage => Append 4 sub nodes', function() {
element(by.id('a-sessionStorage')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(24);
});
it('General Parameters > Sessions > Multiple sessions => Append 6 sub nodes', function() {
element(by.id('a-multipleSessions')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(30);
});
it('General Parameters > Sessions > Persistent sessions => Append 2 sub nodes', function() {
element(by.id('a-persistentSessions')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(32);
});
it('General Parameters > Sessions > Persistent storage options => Append 3 sub nodes', function() {
element(by.id('t-persistentStorageOptions')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(35);
expect(element.all(by.xpath('//tbody/tr')).count()).toEqual(3);
expect(element.all(by.xpath('//tbody/tr/td/input')).count()).toEqual(6);
});
// Configuration reload
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('General Parameters should display 10 sub nodes', function() {
element(by.id('a-generalParameters')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(10);
});
it('General Parameters > Configuration reload => Append 2 sub nodes', function() {
element(by.id('a-reloadParams')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(12);
});
it('General Parameters > Configuration relaod > Reload URLs => "New entry" button + 2 new URLS', function() {
element(by.id('t-reloadUrls')).click();
element(by.css('[ng-click="menuClick(button)"]')).click();
element(by.css('[ng-click="menuClick(button)"]')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(14);
expect(element.all(by.xpath('//tbody/tr')).count()).toEqual(2);
expect(element.all(by.xpath('//tbody/tr/td/input')).count()).toEqual(4);
});
});
});
\ No newline at end of file
'use strict';
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
describe('01 Lemonldap::NG Manager', function() {
describe('Tree display -> Variables', function() {
it('Main => should display 12 main nodes', function() {
browser.get('/');
expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
});
it('Variables should display 3 sub nodes', function() {
element(by.id('a-variables')).click();
expect(element.all(by.repeater('node in node.nodes track by node.id')).count()).toEqual(3);
});
});
});
\ No newline at end of file
...@@ -3,15 +3,11 @@ ...@@ -3,15 +3,11 @@
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
describe('01 Lemonldap::NG Manager', function() { describe('01 Lemonldap::NG Manager', function() {
describe('Tree display', function() { describe('Tree display', function() {
it('Main => should display 12 main nodes', function() {
it('should display 9 main nodes', function() {
browser.get('/'); browser.get('/');
var mainNodes = element.all(by.repeater('node in data track by node.id')); expect(element.all(by.repeater('node in data track by node.id')).count()).toEqual(12);
expect(mainNodes.count()).toEqual(12);
}); });
it('should find a rule', function() { it('should find a rule', function() {
browser.get('/#/confs/1'); browser.get('/#/confs/1');
var vhs = element(by.id('a-virtualHosts')); var vhs = element(by.id('a-virtualHosts'));
...@@ -23,18 +19,5 @@ describe('01 Lemonldap::NG Manager', function() { ...@@ -23,18 +19,5 @@ describe('01 Lemonldap::NG Manager', function() {
var def = element.all(by.id("t-virtualHosts/manager.example.com/locationRules/1")); var def = element.all(by.id("t-virtualHosts/manager.example.com/locationRules/1"));
expect(def.count()).toEqual(1); expect(def.count()).toEqual(1);
}); });
it('should find 19 auth/user modules but only Demo visible', function() {
browser.get('/#/confs/1');
element(by.id('a-generalParameters')).click();
element(by.id('a-authParams')).click();
/* todo */
});
it('should have a hash form if a key is clicked', function() {
element(by.id('a-demoParams')).click();
element(by.id('a-demoExportedVars')).click();
element(by.id('t-demoExportedVars/cn')).click();
var def = element.all(by.id('hashkeyinput'));
expect(def.count()).toEqual(1);
});
}); });
}); });
\ No newline at end of file
...@@ -12,7 +12,7 @@ describe('02 Lemonldap::NG Manager', function() { ...@@ -12,7 +12,7 @@ describe('02 Lemonldap::NG Manager', function() {
"fr": "Paramètres généraux" "fr": "Paramètres généraux"
}; };
var els = element.all(by.css('[ng-click="getLanguage(lang)"]')); var els = element.all(by.css('[ng-click="getLanguage(lang)"]'));
expect(els.count()).toEqual(8); expect(els.count()).toEqual(14);
els.each(function(el) { els.each(function(el) {
el.isDisplayed().then(function(isVisible) { el.isDisplayed().then(function(isVisible) {
if (isVisible) { if (isVisible) {
......
...@@ -9,6 +9,7 @@ describe('08 Lemonldap::NG Manager', function() { ...@@ -9,6 +9,7 @@ describe('08 Lemonldap::NG Manager', function() {
it('should be able to add reload urls', function() { it('should be able to add reload urls', function() {
browser.get('/#/confs/latest'); browser.get('/#/confs/latest');
element(by.id('a-generalParameters')).click(); element(by.id('a-generalParameters')).click();
element(by.id('t-reloadParams')).click();
element(by.id('t-reloadUrls')).click(); element(by.id('t-reloadUrls')).click();
element(by.css('.glyphicon-plus-sign')).click(); element(by.css('.glyphicon-plus-sign')).click();
element(by.id('a-reloadUrls')).click(); element(by.id('a-reloadUrls')).click();
......
...@@ -5,14 +5,13 @@ ...@@ -5,14 +5,13 @@
describe('35 Lemonldap::NG Manager', function() { describe('35 Lemonldap::NG Manager', function() {
it('should be able to restore an old configuration', function() { it('should be able to restore an old configuration', function() {
browser.get('/#/confs/1'); browser.get('/#!/confs/1');
element(by.id('save')).click(); element(by.id('save')).click();
element(by.id('longtextinput')).sendKeys('Restore conf 1'); element(by.id('longtextinput')).sendKeys('Restore conf 1');
element(by.id('saveok')).click(); element(by.id('saveok')).click();
browser.sleep(500);
element(by.id('messageok')).click(); element(by.id('messageok')).click();
browser.sleep(500);
element(by.id('forcesave')).click(); element(by.id('forcesave')).click();
element(by.id('save')).click();
element(by.id('longtextinput')).sendKeys('Force to restore conf 1'); element(by.id('longtextinput')).sendKeys('Force to restore conf 1');
element(by.id('saveok')).click(); element(by.id('saveok')).click();
element(by.id('messageok')).click(); element(by.id('messageok')).click();
......
...@@ -7,7 +7,7 @@ describe('36 Lemonldap::NG Manager', function() { ...@@ -7,7 +7,7 @@ describe('36 Lemonldap::NG Manager', function() {
describe('Diff interface', function() { describe('Diff interface', function() {