Skip to content

Commit 1679a88

Browse files
committed
Merge branch 'master' into release
2 parents c7a3fdc + 657e3e5 commit 1679a88

File tree

7 files changed

+151
-67
lines changed

7 files changed

+151
-67
lines changed

build-system/tasks/presubmit-checks.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,14 @@ var forbiddenTerms = {
6868
'src/cookies.js',
6969
'src/experiments.js',
7070
'test/functional/test-cookies.js',
71-
'tools/experiments/experiments.js',
7271
]
7372
},
7473
'setCookie\\W': {
7574
message: requiresReviewPrivacy,
7675
whitelist: [
7776
'src/cookies.js',
77+
'src/experiments.js',
7878
'test/functional/test-cookies.js',
79-
'tools/experiments/experiments.js',
8079
]
8180
},
8281
'eval\\(': '',

src/experiments.js

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,79 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {getCookie} from './cookies';
17+
/**
18+
* @fileoverview Experiments system allows a developer to opt-in to test
19+
* features that are not yet fully tested.
20+
*
21+
* Experiments page: https://cdn.ampproject.org/experiments.html *
22+
*/
23+
24+
import {getCookie, setCookie} from './cookies';
25+
import {timer} from './timer';
26+
27+
28+
/** @const {string} */
29+
const COOKIE_NAME = 'AMP_EXP';
30+
31+
/** @const {number} */
32+
const COOKIE_MAX_AGE_DAYS = 180; // 6 month
33+
34+
/** @const {time} */
35+
const COOKIE_EXPIRATION_INTERVAL = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
1836

1937

2038
/**
2139
* Whether the specified experiment is on or off.
22-
*
23-
* All experiments are opt-in. A user has to visit the experiments page and
24-
* manually toggle the experiment on.
25-
* TODO(dvoytenko): provide the URL for the experiments page once ready.
26-
*
2740
* @param {!Window} win
2841
* @param {string} experimentId
2942
* @return {boolean}
3043
*/
3144
export function isExperimentOn(win, experimentId) {
32-
const experimentCookie = getCookie(win, 'AMP_EXP');
33-
if (!experimentCookie) {
34-
return false;
45+
return getExperimentIds(win).indexOf(experimentId) != -1;
46+
}
47+
48+
49+
/**
50+
* Toggles the expriment on or off. Returns the actual value of the expriment
51+
* after toggling is done.
52+
* @param {!Window} win
53+
* @param {string} experimentId
54+
* @param {boolean=} opt_on
55+
* @return {boolean}
56+
*/
57+
export function toggleExperiment(win, experimentId, opt_on) {
58+
const experimentIds = getExperimentIds(win);
59+
const currentlyOn = experimentIds.indexOf(experimentId) != -1;
60+
const on = opt_on !== undefined ? opt_on : !currentlyOn;
61+
if (on != currentlyOn) {
62+
if (on) {
63+
experimentIds.push(experimentId);
64+
} else {
65+
experimentIds.splice(experimentIds.indexOf(experimentId), 1);
66+
}
67+
saveExperimentIds(win, experimentIds);
3568
}
36-
return experimentCookie.split(/\s*,\s*/g).indexOf(experimentId) != -1;
69+
return on;
70+
}
71+
72+
73+
/**
74+
* Returns a set of experiment IDs currently on.
75+
* @param {!Window} win
76+
* @return {!Array<string>}
77+
*/
78+
function getExperimentIds(win) {
79+
const experimentCookie = getCookie(win, COOKIE_NAME);
80+
return experimentCookie ? experimentCookie.split(/\s*,\s*/g) : [];
81+
}
82+
83+
84+
/**
85+
* Saves a set of experiment IDs currently on.
86+
* @param {!Window} win
87+
* @param {!Array<string>} experimentIds
88+
*/
89+
function saveExperimentIds(win, experimentIds) {
90+
setCookie(win, COOKIE_NAME, experimentIds.join(','),
91+
timer.now() + COOKIE_EXPIRATION_INTERVAL);
3792
}

src/runtime.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {BaseTemplate, registerExtendedTemplate} from './template';
1919
import {assert} from './asserts';
2020
import {getMode} from './mode';
2121
import {installStyles} from './styles';
22+
import {isExperimentOn, toggleExperiment} from './experiments';
2223
import {performanceFor} from './performance';
2324
import {registerElement} from './custom-element';
2425
import {registerExtendedElement} from './extended-element';
@@ -99,6 +100,10 @@ export function adopt(global) {
99100
global.AMP.toggleRuntime = viewer.toggleRuntime.bind(viewer);
100101
/** @const */
101102
global.AMP.resources = resourcesFor(global);
103+
/** @const */
104+
global.AMP.isExperimentOn = isExperimentOn.bind(null, global);
105+
/** @const */
106+
global.AMP.toggleExperiment = toggleExperiment.bind(null, global);
102107
}
103108

104109
const viewport = viewportFor(global);

test/functional/test-cid.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,12 @@ describe('cid', () => {
146146

147147
it('should pick up the cid value from storage', () => {
148148
storage['amp-cid'] = JSON.stringify({
149-
cid: 'XXX',
149+
cid: 'YYY',
150150
time: timer.now(),
151151
});
152152
return compare(
153153
'e2',
154-
'sha384(XXXhttp://www.origin.come2)');
154+
'sha384(YYYhttp://www.origin.come2)');
155155
});
156156

157157
it('should work without mocking', () => {
@@ -309,8 +309,8 @@ describe('getSourceOrigin', () => {
309309

310310
it('should fail on invalid source origin', () => {
311311
expect(() => {
312-
getSourceOrigin(parseUrl('https://cdn.ampproject.org/v/xxx/'));
313-
}).to.throw(/Expected a \. in origin http:\/\/xxx/);
312+
getSourceOrigin(parseUrl('https://cdn.ampproject.org/v/yyy/'));
313+
}).to.throw(/Expected a \. in origin http:\/\/yyy/);
314314
});
315315

316316
it('should fail on non-proxy origin', () => {

test/functional/test-experiments.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {isExperimentOn} from '../../src/experiments';
17+
import {isExperimentOn, toggleExperiment} from '../../src/experiments';
18+
import * as sinon from 'sinon';
1819

1920

2021
describe('isExperimentOn', () => {
@@ -46,3 +47,66 @@ describe('isExperimentOn', () => {
4647
expectExperiment('AMP_EXP=e2 , e1', 'e1').to.be.true;
4748
});
4849
});
50+
51+
52+
describe('toggleExperiment', () => {
53+
54+
let sandbox;
55+
let clock;
56+
let expTime;
57+
58+
beforeEach(() => {
59+
sandbox = sinon.sandbox.create();
60+
clock = sandbox.useFakeTimers();
61+
clock.tick(1);
62+
expTime = new Date(1 + 180 * 24 * 60 * 60 * 1000).toUTCString();
63+
});
64+
65+
afterEach(() => {
66+
clock.restore();
67+
clock = null;
68+
sandbox.restore();
69+
sandbox = null;
70+
});
71+
72+
function expectToggle(cookiesString, experimentId, opt_on) {
73+
const doc = {
74+
cookie: cookiesString
75+
};
76+
const on = toggleExperiment({document: doc}, experimentId, opt_on);
77+
const parts = doc.cookie.split(/\s*;\s*/g);
78+
if (parts.length > 1) {
79+
expect(parts[1]).to.equal('path=/');
80+
expect(parts[2]).to.equal('expires=' + expTime);
81+
}
82+
return expect(`${on}; ${decodeURIComponent(parts[0])}`);
83+
}
84+
85+
it('should toggle to "on" with no cookies, malformed or empty', () => {
86+
expectToggle(null, 'e1').to.equal('true; AMP_EXP=e1');
87+
expectToggle(undefined, 'e1').to.equal('true; AMP_EXP=e1');
88+
expectToggle('', 'e1').to.equal('true; AMP_EXP=e1');
89+
expectToggle('AMP_EXP', 'e1').to.equal('true; AMP_EXP=e1');
90+
expectToggle('AMP_EXP=', 'e1').to.equal('true; AMP_EXP=e1');
91+
});
92+
93+
it('should toggle "on" when value is not in the list', () => {
94+
expectToggle('AMP_EXP=e1a,e2', 'e1').to.equal('true; AMP_EXP=e1a,e2,e1');
95+
});
96+
97+
it('should toggle "off" when value is in the list', () => {
98+
expectToggle('AMP_EXP=e1', 'e1').to.equal('false; AMP_EXP=');
99+
expectToggle('AMP_EXP=e1,e2', 'e1').to.equal('false; AMP_EXP=e2');
100+
expectToggle('AMP_EXP=e2,e1', 'e1').to.equal('false; AMP_EXP=e2');
101+
});
102+
103+
it('should set "on" when requested', () => {
104+
expectToggle('AMP_EXP=e2', 'e1', true).to.equal('true; AMP_EXP=e2,e1');
105+
expectToggle('AMP_EXP=e1', 'e1', true).to.equal('true; AMP_EXP=e1');
106+
});
107+
108+
it('should set "off" when requested', () => {
109+
expectToggle('AMP_EXP=e2,e1', 'e1', false).to.equal('false; AMP_EXP=e2');
110+
expectToggle('AMP_EXP=e1', 'e1', false).to.equal('false; AMP_EXP=');
111+
});
112+
});

tools/experiments/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ Experiments UI is a available at:
88

99
[https://cdn.ampproject.org/experiments.js](https://cdn.ampproject.org/experiments.js)
1010

11+
Alternatively, the expriments can be toggled in the devtools console in the development
12+
mode using:
13+
```
14+
AMP.toggleExperiment('experiment name')
15+
```

tools/experiments/experiments.js

Lines changed: 6 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616

1717
import '../../src/polyfills';
1818
import {onDocumentReady} from '../../src/document-state';
19-
import {getCookie, setCookie} from '../../src/cookies';
20-
21-
22-
/** @const {number} */
23-
const COOKIE_MAX_AGE_DAYS = 180; // 6 month
19+
import {isExperimentOn, toggleExperiment} from '../../src/experiments';
2420

2521

2622
/**
@@ -90,8 +86,10 @@ function buildExperimentRow(experiment) {
9086
buttonOff.textContent = 'Off';
9187
button.appendChild(buttonOff);
9288

93-
button.addEventListener('click', toggleExperiment.bind(null, experiment.id,
94-
undefined));
89+
button.addEventListener('click', () => {
90+
toggleExperiment(window, experiment.id);
91+
update();
92+
});
9593

9694
return tr;
9795
}
@@ -136,49 +134,7 @@ function updateExperimentRow(experiment) {
136134
if (!tr) {
137135
return;
138136
}
139-
tr.setAttribute('data-on', isExperimentOn(experiment.id) ? 1 : 0);
140-
}
141-
142-
143-
/**
144-
* Returns a set of experiment IDs currently on.
145-
* @return {!Array<string>}
146-
*/
147-
function getExperimentIds() {
148-
const experimentCookie = getCookie(window, 'AMP_EXP');
149-
return experimentCookie ? experimentCookie.split(/\s*,\s*/g) : [];
150-
}
151-
152-
153-
/**
154-
* Returns whether the experiment is on or off.
155-
* @param {string} id
156-
* @return {boolean}
157-
*/
158-
function isExperimentOn(id) {
159-
return getExperimentIds().indexOf(id) != -1;
160-
}
161-
162-
163-
/**
164-
* Toggles the expriment.
165-
* @param {string} id
166-
* @param {boolean=} opt_on
167-
*/
168-
function toggleExperiment(id, opt_on) {
169-
const experimentIds = getExperimentIds();
170-
const currentlyOn = experimentIds.indexOf(id) != -1;
171-
const on = opt_on !== undefined ? opt_on : !currentlyOn;
172-
if (on != currentlyOn) {
173-
if (on) {
174-
experimentIds.push(id);
175-
} else {
176-
experimentIds.splice(experimentIds.indexOf(id), 1);
177-
}
178-
setCookie(window, 'AMP_EXP', experimentIds.join(','),
179-
new Date().getTime() + COOKIE_MAX_AGE_DAYS * 24 * 60 * 60 * 1000);
180-
}
181-
update();
137+
tr.setAttribute('data-on', isExperimentOn(window, experiment.id) ? 1 : 0);
182138
}
183139

184140

0 commit comments

Comments
 (0)