Skip to content

Commit 7959b22

Browse files
authored
🚀 Delay marking unresolved AMP Elements until extension fails (#36157)
1 parent 4e2ac0c commit 7959b22

8 files changed

Lines changed: 113 additions & 16 deletions

File tree

‎extensions/amp-a4a/0.1/test/test-template-renderer.js‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,8 @@ describes.realWin('TemplateRenderer', realWinConfig, (env) => {
115115
'"https://buy.com/buy-1">Click for ad!</a>' +
116116
'\n <amp-analytics class="i-amphtml-element i-amphtml' +
117117
'-notbuilt amp-notbuilt i-amphtml-layout-fixed i-amphtml' +
118-
'-layout-size-defined amp-unresolved i-amphtml-' +
119-
'unresolved" i-amphtml-layout="fixed" style="width: 1px;' +
120-
' height: 1px;"></amp-analytics></div>'
118+
'-layout-size-defined" i-amphtml-layout="fixed" style="width: ' +
119+
'1px; height: 1px;"></amp-analytics></div>'
121120
);
122121
});
123122
});

‎src/amp.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import './polyfills';
77

88
import {TickLabel} from '#core/constants/enums';
9+
import {whenDocumentComplete} from '#core/document-ready';
910
import * as mode from '#core/mode';
1011

1112
import {Services} from '#service';
@@ -21,6 +22,7 @@ import {installPlatformService} from '#service/platform-impl';
2122

2223
import {installAutoLightboxExtension} from './auto-lightbox';
2324
import {startupChunk} from './chunk';
25+
import {markUnresolvedElements} from './custom-element';
2426
import {installErrorReporting} from './error-reporting';
2527
import {fontStylesheetTimeout} from './font-stylesheet-timeout';
2628
import {maybeTrackImpression} from './impression';
@@ -63,6 +65,7 @@ function bootstrap(ampdoc, perf) {
6365
startupChunk(self.document, function stub() {
6466
// Pre-stub already known elements.
6567
stubElementsForDoc(ampdoc);
68+
whenDocumentComplete(self.document).then(() => markUnresolvedElements());
6669
});
6770
startupChunk(
6871
self.document,

‎src/custom-element.js‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,17 @@ const RETURN_TRUE = () => true;
5555
*/
5656
let templateTagSupported;
5757

58-
/** @type {!Array} */
58+
/** @type {!Array<AmpElement>} */
5959
export const stubbedElements = [];
6060

61+
/**
62+
* Extensions which have failed to load, making their elements unresolvable.
63+
* If null, then any remaining elements which don't immediately have their
64+
* implClass available are marked unresolvable.
65+
* @type {Set<string>}
66+
*/
67+
const unresolvableExtensions = new Set();
68+
6169
/**
6270
* Whether this platform supports template tags.
6371
* @return {boolean}
@@ -367,6 +375,18 @@ function createBaseCustomElementClass(win, elementConnectedCallback) {
367375
}
368376
}
369377

378+
/**
379+
* When the document is ready (meaning all external resources are loaded or
380+
* failed), we mark any stubbed elements as unresolved. If they haven't
381+
* been upgraded yet (or pending upgrade or deferredBuild elements), then
382+
* the extension failed to load.
383+
*/
384+
markUnresolved() {
385+
if (!this.implClass_) {
386+
this.classList.add('amp-unresolved', 'i-amphtml-unresolved');
387+
}
388+
}
389+
370390
/**
371391
* Time delay imposed by baseElement upgradeCallback. If no
372392
* upgradeCallback specified or not yet executed, delay is 0.
@@ -1182,11 +1202,17 @@ function createBaseCustomElementClass(win, elementConnectedCallback) {
11821202
} catch (e) {
11831203
reportError(e, this);
11841204
}
1205+
11851206
if (this.implClass_) {
11861207
this.upgradeOrSchedule_();
1208+
} else if (
1209+
unresolvableExtensions.has('*') ||
1210+
unresolvableExtensions.has(this.tagName.toLowerCase())
1211+
) {
1212+
this.markUnresolved();
11871213
}
1214+
11881215
if (!this.isUpgraded()) {
1189-
this.classList.add('amp-unresolved', 'i-amphtml-unresolved');
11901216
this.dispatchCustomEventForTesting(AmpEvents.STUBBED);
11911217
}
11921218
}
@@ -2175,3 +2201,21 @@ export function getImplSyncForTesting(element) {
21752201
export function getActionQueueForTesting(element) {
21762202
return element.actionQueue_;
21772203
}
2204+
2205+
/**
2206+
* Marks each element that still stubbed as unresolved.
2207+
* @param {string=} opt_extension
2208+
*/
2209+
export function markUnresolvedElements(opt_extension) {
2210+
unresolvableExtensions.add(opt_extension || '*');
2211+
for (const el of stubbedElements) {
2212+
if (opt_extension == null || el.tagName.toLowerCase() === opt_extension) {
2213+
el.markUnresolved();
2214+
}
2215+
}
2216+
}
2217+
2218+
/** */
2219+
export function resetUnresolvedElementsForTesting() {
2220+
unresolvableExtensions.clear();
2221+
}

‎src/service/custom-element-registry.js‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import {Services} from '#service';
22

33
import {extensionScriptsInNode} from './extension-script';
44

5-
import {createCustomElementClass, stubbedElements} from '../custom-element';
5+
import {
6+
createCustomElementClass,
7+
markUnresolvedElements,
8+
stubbedElements,
9+
} from '../custom-element';
610
import {ElementStub} from '../element-stub';
711
import {reportError} from '../error-reporting';
812
import {userAssert} from '../log';
@@ -120,8 +124,9 @@ function waitReadyForUpgrade(win, elementClass) {
120124
*/
121125
export function stubElementsForDoc(ampdoc) {
122126
const extensions = extensionScriptsInNode(ampdoc.getHeadNode());
123-
extensions.forEach(({extensionId, extensionVersion}) => {
127+
extensions.forEach(({extensionId, extensionVersion, script}) => {
124128
ampdoc.declareExtension(extensionId, extensionVersion);
129+
script.addEventListener('error', () => markUnresolvedElements(extensionId));
125130
stubElementIfNotKnown(ampdoc.win, extensionId);
126131
});
127132
if (ampdoc.isBodyAvailable()) {

‎src/service/extension-script.js‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function getExtensionScripts(
197197
/**
198198
* Get list of all the extension JS files.
199199
* @param {HTMLHeadElement|Element|ShadowRoot|Document} head
200-
* @return {!Array<{extensionId: string, extensionVersion: string}>}
200+
* @return {!Array<{script: HTMLScriptElement, extensionId: string, extensionVersion: string}>}
201201
*/
202202
export function extensionScriptsInNode(head) {
203203
// ampdoc.getHeadNode() can return null.
@@ -216,9 +216,12 @@ export function extensionScriptsInNode(head) {
216216
script.getAttribute('custom-element') ||
217217
script.getAttribute('custom-template');
218218
const urlParts = parseExtensionUrl(script.src);
219-
// TODO: register error handler
220219
if (extensionId && urlParts) {
221-
scripts.push({extensionId, extensionVersion: urlParts.extensionVersion});
220+
scripts.push({
221+
script,
222+
extensionId,
223+
extensionVersion: urlParts.extensionVersion,
224+
});
222225
}
223226
}
224227
return scripts;

‎src/service/performance-impl.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export class Performance {
144144
* See https://github.com/GoogleChrome/web-vitals/blob/main/src/getCLS.ts
145145
* @private {Array<LayoutShift>}
146146
*/
147-
this.layoutShiftEntires_ = [];
147+
this.layoutShiftEntries_ = [];
148148

149149
/**
150150
* The sum of all layout shifts.
@@ -559,7 +559,7 @@ export class Performance {
559559
if (this.isVisibilityHidden_()) {
560560
return;
561561
}
562-
const entries = this.layoutShiftEntires_;
562+
const entries = this.layoutShiftEntries_;
563563
if (entries.length > 0) {
564564
const first = entries[0];
565565
const last = entries[entries.length - 1];
@@ -586,7 +586,7 @@ export class Performance {
586586
* See https://web.dev/evolving-cls/
587587
*/
588588
flushLayoutShiftScore_() {
589-
const entries = this.layoutShiftEntires_;
589+
const entries = this.layoutShiftEntries_;
590590
const old = this.metrics_.get(TickLabel.CUMULATIVE_LAYOUT_SHIFT);
591591
let union = 0;
592592
let sum = 0;

‎test/unit/test-custom-element-registry.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ describes.realWin('CustomElement register', {amp: true}, (env) => {
262262
hasAttribute(name) {
263263
return name == 'custom-element' || name == 'src';
264264
},
265+
addEventListener() {},
265266
src: 'https://cdn.ampproject.org/v0/amp-test1-0.2.js',
266267
ownerDocument: doc,
267268
};
@@ -326,6 +327,7 @@ describes.realWin('CustomElement register', {amp: true}, (env) => {
326327
hasAttribute(name) {
327328
return name == 'custom-element' || name == 'src';
328329
},
330+
addEventListener() {},
329331
src: 'https://cdn.ampproject.org/v0/amp-test2-0.3.js',
330332
ownerDocument: doc,
331333
};

‎test/unit/test-custom-element.js‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {chunkInstanceForTesting} from '../../src/chunk';
1313
import {
1414
createAmpElementForTesting,
1515
getImplSyncForTesting,
16+
markUnresolvedElements,
17+
resetUnresolvedElementsForTesting,
1618
} from '../../src/custom-element';
1719
import {ElementStub} from '../../src/element-stub';
1820

@@ -153,6 +155,7 @@ describes.realWin('CustomElement', {amp: true}, (env) => {
153155
afterEach(() => {
154156
clock.uninstall();
155157
resourcesMock.verify();
158+
resetUnresolvedElementsForTesting();
156159
});
157160

158161
function skipMicroTask() {
@@ -948,9 +951,9 @@ describes.realWin('CustomElement', {amp: true}, (env) => {
948951

949952
expect(element.everAttached).to.equal(true);
950953
expect(element.getLayout()).to.equal(Layout.FILL);
951-
// Not upgraded yet!
952-
expect(element).to.have.class('amp-unresolved');
953-
expect(element).to.have.class('i-amphtml-unresolved');
954+
// Not upgraded yet, but extension hasn't failed.
955+
expect(element).not.to.have.class('amp-unresolved');
956+
expect(element).not.to.have.class('i-amphtml-unresolved');
954957

955958
// Upgrade
956959
resourcesMock.expects('upgraded').withExactArgs(element).once();
@@ -967,6 +970,44 @@ describes.realWin('CustomElement', {amp: true}, (env) => {
967970
expect(element).to.not.have.class('i-amphtml-unresolved');
968971
});
969972

973+
it('StubElement - attachedCallback after failed to load', () => {
974+
const element = new StubElementClass();
975+
markUnresolvedElements('amp-stub');
976+
element.setAttribute('layout', 'fill');
977+
expect(element.everAttached).to.equal(false);
978+
expect(element.getLayout()).to.equal(Layout.NODISPLAY);
979+
980+
resourcesMock.expects('add').withExactArgs(element).atLeast(1);
981+
container.appendChild(element);
982+
983+
expect(element.everAttached).to.equal(true);
984+
expect(element.getLayout()).to.equal(Layout.FILL);
985+
// Extension already failed before attachedCallback
986+
expect(element).to.have.class('amp-unresolved');
987+
expect(element).to.have.class('i-amphtml-unresolved');
988+
});
989+
990+
it('StubElement - attachedCallback before failed to load', () => {
991+
const element = new StubElementClass();
992+
element.setAttribute('layout', 'fill');
993+
expect(element.everAttached).to.equal(false);
994+
expect(element.getLayout()).to.equal(Layout.NODISPLAY);
995+
996+
resourcesMock.expects('add').withExactArgs(element).atLeast(1);
997+
container.appendChild(element);
998+
999+
expect(element.everAttached).to.equal(true);
1000+
expect(element.getLayout()).to.equal(Layout.FILL);
1001+
// Not upgraded yet, but extension hasn't failed.
1002+
expect(element).not.to.have.class('amp-unresolved');
1003+
expect(element).not.to.have.class('i-amphtml-unresolved');
1004+
1005+
// Now it's called.
1006+
markUnresolvedElements('amp-stub');
1007+
expect(element).to.have.class('amp-unresolved');
1008+
expect(element).to.have.class('i-amphtml-unresolved');
1009+
});
1010+
9701011
it('Element - detachedCallback', () => {
9711012
const element = new ElementClass();
9721013
element.setAttribute('layout', 'fill');

0 commit comments

Comments
 (0)