Skip to content

Commit 99b6cb5

Browse files
authored
Viewer get/setDocumentState messaging. (ampproject#26285)
* Viewer get/setDocumentState messaging. * Move 'startListening' into 'initializeListeners_'.
1 parent f050e01 commit 99b6cb5

File tree

6 files changed

+320
-12
lines changed

6 files changed

+320
-12
lines changed

build-system/tasks/presubmit-checks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ const forbiddenTerms = {
383383
'extensions/amp-subscriptions/0.1/viewer-subscription-platform.js',
384384
'extensions/amp-viewer-integration/0.1/highlight-handler.js',
385385
'extensions/amp-consent/0.1/consent-ui.js',
386+
'extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js',
386387

387388
// iframe-messaging-client.sendMessage
388389
'3p/iframe-messaging-client.js',

build-system/test-configs/conformance-config.textproto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ requirement: {
4040
error_message: 'History.p.state is broken in IE11. Please use the helper methods provided in src/history.js'
4141
value: 'History.prototype.state'
4242
whitelist: 'src/history.js'
43+
whitelist: 'extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js'
4344
}
4445

4546
# Strings
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
Action,
19+
StateProperty,
20+
getStoreService,
21+
} from './amp-story-store-service';
22+
23+
/** @typedef {{property: !StateProperty}} */
24+
let GetStateConfigurationDef;
25+
26+
// TODO(#26020): implement and allow retrieving PAGE_ATTACHMENT_STATE.
27+
// TODO(gmajoulet): implement and allow retrieving STORY_PROGRESS.
28+
/** @enum {!GetStateConfigurationDef} */
29+
const GET_STATE_CONFIGURATIONS = {
30+
'CURRENT_PAGE_ID': {
31+
property: StateProperty.CURRENT_PAGE_ID,
32+
},
33+
'MUTED_STATE': {
34+
property: StateProperty.MUTED_STATE,
35+
},
36+
};
37+
38+
/** @typedef {{action: !Action, isValueValid: function(*):boolean}} */
39+
let SetStateConfigurationDef;
40+
41+
/** @enum {!SetStateConfigurationDef} */
42+
const SET_STATE_CONFIGURATIONS = {
43+
'MUTED_STATE': {
44+
action: Action.TOGGLE_MUTED,
45+
isValueValid: value => typeof value === 'boolean',
46+
},
47+
};
48+
49+
/**
50+
* Viewer messaging handler.
51+
*/
52+
export class AmpStoryViewerMessagingHandler {
53+
/**
54+
* @param {!Window} win
55+
* @param {!../../../src/service/viewer-interface.ViewerInterface} viewer
56+
*/
57+
constructor(win, viewer) {
58+
/** @private @const {!./amp-story-store-service.AmpStoryStoreService} */
59+
this.storeService_ = getStoreService(win);
60+
61+
/** @private @const {!../../../src/service/viewer-interface.ViewerInterface} */
62+
this.viewer_ = viewer;
63+
}
64+
65+
/**
66+
* @public
67+
*/
68+
startListening() {
69+
this.viewer_.onMessageRespond('getDocumentState', data =>
70+
this.onGetDocumentState_(data)
71+
);
72+
this.viewer_.onMessageRespond('setDocumentState', data =>
73+
this.onSetDocumentState_(data)
74+
);
75+
}
76+
77+
/**
78+
* @param {string} eventType
79+
* @param {?JsonObject|string|undefined} data
80+
* @param {boolean=} cancelUnsent
81+
*/
82+
send(eventType, data, cancelUnsent = false) {
83+
this.viewer_.sendMessage(eventType, data, cancelUnsent);
84+
}
85+
86+
/**
87+
* Handles 'getDocumentState' viewer messages.
88+
* @param {!Object=} data
89+
* @return {!Promise}
90+
* @private
91+
*/
92+
onGetDocumentState_(data = {}) {
93+
const {state} = data;
94+
const config = GET_STATE_CONFIGURATIONS[state];
95+
96+
if (!config) {
97+
return Promise.reject(`Invalid 'state' parameter`);
98+
}
99+
100+
const value = this.storeService_.get(config.property);
101+
102+
return Promise.resolve({state, value});
103+
}
104+
105+
/**
106+
* Handles 'setDocumentState' viewer messages.
107+
* @param {!Object=} data
108+
* @return {!Promise<!Object|undefined>}
109+
* @private
110+
*/
111+
onSetDocumentState_(data = {}) {
112+
const {state, value} = data;
113+
const config = SET_STATE_CONFIGURATIONS[state];
114+
115+
if (!config) {
116+
return Promise.reject(`Invalid 'state' parameter`);
117+
}
118+
119+
if (!config.isValueValid(value)) {
120+
return Promise.reject(`Invalid 'value' parameter`);
121+
}
122+
123+
this.storeService_.dispatch(config.action, value);
124+
125+
return Promise.resolve({state, value});
126+
}
127+
}

extensions/amp-story/1.0/amp-story.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {AmpStoryPage, NavigationDirection, PageState} from './amp-story-page';
5454
import {AmpStoryPageAttachment} from './amp-story-page-attachment';
5555
import {AmpStoryQuiz} from './amp-story-quiz';
5656
import {AmpStoryRenderService} from './amp-story-render-service';
57+
import {AmpStoryViewerMessagingHandler} from './amp-story-viewer-messaging-handler';
5758
import {AnalyticsVariable, getVariableService} from './variable-service';
5859
import {CSS} from '../../../build/amp-story-1.0.css';
5960
import {CommonSignals} from '../../../src/common-signals';
@@ -336,6 +337,11 @@ export class AmpStory extends AMP.BaseElement {
336337
/** @private @const {!../../../src/service/viewer-interface.ViewerInterface} */
337338
this.viewer_ = Services.viewerForDoc(this.element);
338339

340+
/** @private @const {?AmpStoryViewerMessagingHandler} */
341+
this.viewerMessagingHandler_ = this.viewer_.isEmbedded()
342+
? new AmpStoryViewerMessagingHandler(this.win, this.viewer_)
343+
: null;
344+
339345
/**
340346
* Store the current paused state, to make sure the story does not play on
341347
* resume if it was previously paused.
@@ -832,7 +838,14 @@ export class AmpStory extends AMP.BaseElement {
832838
this.getViewport().onResize(debounce(this.win, () => this.onResize(), 300));
833839
this.installGestureRecognizers_();
834840

841+
// TODO(gmajoulet): migrate this to amp-story-viewer-messaging-handler once
842+
// there is a way to navigate to pages that does not involve using private
843+
// amp-story methods.
835844
this.viewer_.onMessage('selectPage', data => this.onSelectPage_(data));
845+
846+
if (this.viewerMessagingHandler_) {
847+
this.viewerMessagingHandler_.startListening();
848+
}
836849
}
837850

838851
/** @private */
@@ -1323,8 +1336,8 @@ export class AmpStory extends AMP.BaseElement {
13231336
* @private
13241337
*/
13251338
onNoNextPage_() {
1326-
if (this.viewer_.hasCapability('swipe')) {
1327-
this.viewer_./*OK*/ sendMessage('selectDocument', dict({'next': true}));
1339+
if (this.viewer_.hasCapability('swipe') && this.viewerMessagingHandler_) {
1340+
this.viewerMessagingHandler_.send('selectDocument', dict({'next': true}));
13281341
return;
13291342
}
13301343

@@ -1352,8 +1365,8 @@ export class AmpStory extends AMP.BaseElement {
13521365
* @private
13531366
*/
13541367
onNoPreviousPage_() {
1355-
if (this.viewer_.hasCapability('swipe')) {
1356-
this.viewer_./*OK*/ sendMessage(
1368+
if (this.viewer_.hasCapability('swipe') && this.viewerMessagingHandler_) {
1369+
this.viewerMessagingHandler_.send(
13571370
'selectDocument',
13581371
dict({'previous': true})
13591372
);
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
Action,
19+
StateProperty,
20+
getStoreService,
21+
} from '../amp-story-store-service';
22+
import {AmpStoryViewerMessagingHandler} from '../amp-story-viewer-messaging-handler';
23+
24+
describes.fakeWin('amp-story-viewer-messaging-handler', {}, env => {
25+
let fakeViewerService;
26+
let storeService;
27+
let viewerMessagingHandler;
28+
29+
beforeEach(() => {
30+
fakeViewerService = {
31+
responderMap: {},
32+
onMessageRespond(eventType, responder) {
33+
this.responderMap[eventType] = responder;
34+
},
35+
receiveMessage(eventType, data) {
36+
if (!this.responderMap[eventType]) {
37+
return;
38+
}
39+
return this.responderMap[eventType](data);
40+
},
41+
};
42+
viewerMessagingHandler = new AmpStoryViewerMessagingHandler(
43+
env.win,
44+
fakeViewerService
45+
);
46+
viewerMessagingHandler.startListening();
47+
storeService = getStoreService(env.win);
48+
});
49+
50+
describe('getDocumentState', () => {
51+
it('should throw if no state', async () => {
52+
try {
53+
await fakeViewerService.receiveMessage('getDocumentState', undefined);
54+
return Promise.reject('Previous line should throw an error');
55+
} catch (error) {
56+
expect(error).to.equal(`Invalid 'state' parameter`);
57+
}
58+
});
59+
60+
it('should throw if invalid state', async () => {
61+
try {
62+
await fakeViewerService.receiveMessage('getDocumentState', {
63+
state: 'UNEXISTING_STATE',
64+
});
65+
return Promise.reject('Previous line should throw an error');
66+
} catch (error) {
67+
expect(error).to.equal(`Invalid 'state' parameter`);
68+
}
69+
});
70+
71+
it('should return the MUTED_STATE', async () => {
72+
const response = await fakeViewerService.receiveMessage(
73+
'getDocumentState',
74+
{state: 'MUTED_STATE'}
75+
);
76+
expect(response).to.deep.equal({
77+
state: 'MUTED_STATE',
78+
value: storeService.get(StateProperty.MUTED_STATE),
79+
});
80+
});
81+
82+
it('should return the CURRENT_PAGE_ID', async () => {
83+
storeService.dispatch(Action.CHANGE_PAGE, {id: 'foo', index: 0});
84+
const response = await fakeViewerService.receiveMessage(
85+
'getDocumentState',
86+
{state: 'CURRENT_PAGE_ID'}
87+
);
88+
expect(response).to.deep.equal({
89+
state: 'CURRENT_PAGE_ID',
90+
value: storeService.get(StateProperty.CURRENT_PAGE_ID),
91+
});
92+
});
93+
});
94+
95+
describe('setDocumentState', () => {
96+
it('should throw if no state', async () => {
97+
try {
98+
await fakeViewerService.receiveMessage('setDocumentState', undefined);
99+
return Promise.reject('Previous line should throw an error');
100+
} catch (error) {
101+
expect(error).to.equal(`Invalid 'state' parameter`);
102+
}
103+
});
104+
105+
it('should throw if invalid state', async () => {
106+
try {
107+
await fakeViewerService.receiveMessage('setDocumentState', {
108+
state: 'UNEXISTING_STATE',
109+
value: true,
110+
});
111+
return Promise.reject('Previous line should throw an error');
112+
} catch (error) {
113+
expect(error).to.equal(`Invalid 'state' parameter`);
114+
}
115+
});
116+
117+
it('should throw if no value', async () => {
118+
try {
119+
await fakeViewerService.receiveMessage('setDocumentState', {
120+
state: 'MUTED_STATE',
121+
});
122+
return Promise.reject('Previous line should throw an error');
123+
} catch (error) {
124+
expect(error).to.equal(`Invalid 'value' parameter`);
125+
}
126+
});
127+
128+
it('should throw if invalid value', async () => {
129+
try {
130+
await fakeViewerService.receiveMessage('setDocumentState', {
131+
state: 'MUTED_STATE',
132+
value: 'true' /** only accepts booleans */,
133+
});
134+
return Promise.reject('Previous line should throw an error');
135+
} catch (error) {
136+
expect(error).to.equal(`Invalid 'value' parameter`);
137+
}
138+
});
139+
140+
it('should set a state', async () => {
141+
storeService.dispatch(Action.TOGGLE_MUTED, false);
142+
await fakeViewerService.receiveMessage('setDocumentState', {
143+
state: 'MUTED_STATE',
144+
value: true,
145+
});
146+
expect(storeService.get(StateProperty.MUTED_STATE)).to.be.true;
147+
});
148+
});
149+
});

0 commit comments

Comments
 (0)