lighthouse 11.7.0-dev.20240409 → 11.7.0-dev.20240410

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.
Files changed (100) hide show
  1. package/cli/cli-flags.js +3 -3
  2. package/cli/test/smokehouse/core-tests.js +0 -16
  3. package/cli/test/smokehouse/frontends/smokehouse-bin.js +2 -2
  4. package/core/config/default-config.js +6 -50
  5. package/core/lib/lantern/network-node.js +28 -4
  6. package/dist/report/bundle.esm.js +11 -95
  7. package/dist/report/flow.js +16 -100
  8. package/dist/report/standalone.js +12 -96
  9. package/flow-report/src/help-dialog.tsx +0 -1
  10. package/flow-report/src/i18n/i18n.d.ts +0 -4
  11. package/flow-report/src/i18n/ui-strings.d.ts +0 -1
  12. package/flow-report/src/i18n/ui-strings.js +0 -2
  13. package/package.json +1 -1
  14. package/readme.md +2 -2
  15. package/report/assets/styles.css +1 -43
  16. package/report/assets/templates.html +0 -95
  17. package/report/renderer/components.d.ts +2 -2
  18. package/report/renderer/components.js +2 -103
  19. package/report/renderer/report-renderer.js +2 -12
  20. package/report/renderer/report-ui-features.js +1 -1
  21. package/report/renderer/report-utils.d.ts +0 -1
  22. package/report/renderer/report-utils.js +0 -3
  23. package/shared/localization/locales/ar-XB.json +0 -174
  24. package/shared/localization/locales/ar.json +0 -174
  25. package/shared/localization/locales/bg.json +0 -174
  26. package/shared/localization/locales/ca.json +0 -174
  27. package/shared/localization/locales/cs.json +0 -174
  28. package/shared/localization/locales/da.json +0 -174
  29. package/shared/localization/locales/de.json +0 -174
  30. package/shared/localization/locales/el.json +0 -174
  31. package/shared/localization/locales/en-GB.json +0 -174
  32. package/shared/localization/locales/en-US.json +0 -177
  33. package/shared/localization/locales/en-XA.json +0 -174
  34. package/shared/localization/locales/en-XL.json +0 -177
  35. package/shared/localization/locales/es-419.json +0 -174
  36. package/shared/localization/locales/es.json +0 -174
  37. package/shared/localization/locales/fi.json +0 -174
  38. package/shared/localization/locales/fil.json +0 -174
  39. package/shared/localization/locales/fr.json +0 -174
  40. package/shared/localization/locales/he.json +0 -174
  41. package/shared/localization/locales/hi.json +0 -174
  42. package/shared/localization/locales/hr.json +0 -174
  43. package/shared/localization/locales/hu.json +0 -174
  44. package/shared/localization/locales/id.json +0 -174
  45. package/shared/localization/locales/it.json +0 -174
  46. package/shared/localization/locales/ja.json +0 -174
  47. package/shared/localization/locales/ko.json +0 -174
  48. package/shared/localization/locales/lt.json +0 -174
  49. package/shared/localization/locales/lv.json +0 -174
  50. package/shared/localization/locales/nl.json +0 -174
  51. package/shared/localization/locales/no.json +0 -174
  52. package/shared/localization/locales/pl.json +0 -174
  53. package/shared/localization/locales/pt-PT.json +0 -174
  54. package/shared/localization/locales/pt.json +0 -174
  55. package/shared/localization/locales/ro.json +0 -174
  56. package/shared/localization/locales/ru.json +0 -174
  57. package/shared/localization/locales/sk.json +0 -174
  58. package/shared/localization/locales/sl.json +0 -174
  59. package/shared/localization/locales/sr-Latn.json +0 -174
  60. package/shared/localization/locales/sr.json +0 -174
  61. package/shared/localization/locales/sv.json +0 -174
  62. package/shared/localization/locales/ta.json +0 -174
  63. package/shared/localization/locales/te.json +0 -174
  64. package/shared/localization/locales/th.json +0 -174
  65. package/shared/localization/locales/tr.json +0 -174
  66. package/shared/localization/locales/uk.json +0 -174
  67. package/shared/localization/locales/vi.json +0 -174
  68. package/shared/localization/locales/zh-HK.json +0 -174
  69. package/shared/localization/locales/zh-TW.json +0 -174
  70. package/shared/localization/locales/zh.json +0 -174
  71. package/types/artifacts.d.ts +0 -27
  72. package/types/audit.d.ts +0 -10
  73. package/types/internal/smokehouse.d.ts +1 -1
  74. package/core/audits/content-width.d.ts +0 -17
  75. package/core/audits/content-width.js +0 -76
  76. package/core/audits/installable-manifest.d.ts +0 -111
  77. package/core/audits/installable-manifest.js +0 -248
  78. package/core/audits/manual/pwa-cross-browser.d.ts +0 -12
  79. package/core/audits/manual/pwa-cross-browser.js +0 -40
  80. package/core/audits/manual/pwa-each-page-has-url.d.ts +0 -12
  81. package/core/audits/manual/pwa-each-page-has-url.js +0 -38
  82. package/core/audits/manual/pwa-page-transitions.d.ts +0 -12
  83. package/core/audits/manual/pwa-page-transitions.js +0 -38
  84. package/core/audits/maskable-icon.d.ts +0 -25
  85. package/core/audits/maskable-icon.js +0 -70
  86. package/core/audits/multi-check-audit.d.ts +0 -28
  87. package/core/audits/multi-check-audit.js +0 -82
  88. package/core/audits/splash-screen.d.ts +0 -37
  89. package/core/audits/splash-screen.js +0 -98
  90. package/core/audits/themed-omnibox.d.ts +0 -42
  91. package/core/audits/themed-omnibox.js +0 -102
  92. package/core/computed/manifest-values.d.ts +0 -24
  93. package/core/computed/manifest-values.js +0 -136
  94. package/core/gather/gatherers/installability-errors.d.ts +0 -17
  95. package/core/gather/gatherers/installability-errors.js +0 -56
  96. package/core/gather/gatherers/web-app-manifest.d.ts +0 -32
  97. package/core/gather/gatherers/web-app-manifest.js +0 -106
  98. package/report/renderer/pwa-category-renderer.d.ts +0 -55
  99. package/report/renderer/pwa-category-renderer.js +0 -184
  100. package/third-party/chromium-synchronization/installability-errors-test.js +0 -79
@@ -1,106 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2021 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
-
7
- import log from 'lighthouse-logger';
8
-
9
- import {parseManifest} from '../../lib/manifest-parser.js';
10
- import BaseGatherer from '../base-gatherer.js';
11
-
12
- class WebAppManifest extends BaseGatherer {
13
- /** @type {LH.Gatherer.GathererMeta} */
14
- meta = {
15
- supportedModes: ['snapshot', 'navigation'],
16
- };
17
-
18
- /**
19
- * @param {LH.Gatherer.ProtocolSession} session
20
- * @return {Promise<{url: string, data: string}|null>}
21
- */
22
- static async fetchAppManifest(session) {
23
- // In all environments but LR, Page.getAppManifest finishes very quickly.
24
- // In LR, there is a bug that causes this command to hang until outgoing
25
- // requests finish. This has been seen in long polling (where it will never
26
- // return) and when other requests take a long time to finish. We allow 10 seconds
27
- // for outgoing requests to finish. Anything more, and we continue the run without
28
- // a manifest.
29
- // Googlers, see: http://b/124008171
30
- session.setNextProtocolTimeout(10000);
31
- let response;
32
- try {
33
- response = await session.sendCommand('Page.getAppManifest');
34
- } catch (err) {
35
- if (err.code === 'PROTOCOL_TIMEOUT') {
36
- // LR will timeout fetching the app manifest in some cases, move on without one.
37
- // https://github.com/GoogleChrome/lighthouse/issues/7147#issuecomment-461210921
38
- log.error('WebAppManifest', 'Failed fetching manifest', err);
39
- return null;
40
- }
41
-
42
- throw err;
43
- }
44
-
45
- let data = response.data;
46
-
47
- // We're not reading `response.errors` however it may contain critical and noncritical
48
- // errors from Blink's manifest parser:
49
- // https://chromedevtools.github.io/debugger-protocol-viewer/tot/Page/#type-AppManifestError
50
- if (!data) {
51
- // If the data is empty, the page had no manifest.
52
- return null;
53
- }
54
-
55
- const BOM_LENGTH = 3;
56
- const BOM_FIRSTCHAR = 65279;
57
- const isBomEncoded = data.charCodeAt(0) === BOM_FIRSTCHAR;
58
-
59
- if (isBomEncoded) {
60
- data = Buffer.from(data)
61
- .slice(BOM_LENGTH)
62
- .toString();
63
- }
64
-
65
- return {...response, data};
66
- }
67
-
68
- /**
69
- * Uses the debugger protocol to fetch the manifest from within the context of
70
- * the target page, reusing any credentials, emulation, etc, already established
71
- * there.
72
- *
73
- * Returns the parsed manifest or null if the page had no manifest. If the manifest
74
- * was unparseable as JSON, manifest.value will be undefined and manifest.warning
75
- * will have the reason. See manifest-parser.js for more information.
76
- *
77
- * @param {LH.Gatherer.ProtocolSession} session
78
- * @param {string} pageUrl
79
- * @return {Promise<LH.Artifacts.Manifest|null>}
80
- */
81
- static async getWebAppManifest(session, pageUrl) {
82
- const status = {msg: 'Get webapp manifest', id: 'lh:gather:getWebAppManifest'};
83
- log.time(status);
84
- const response = await WebAppManifest.fetchAppManifest(session);
85
- if (!response) return null;
86
- const manifest = parseManifest(response.data, response.url, pageUrl);
87
- log.timeEnd(status);
88
- return manifest;
89
- }
90
-
91
- /**
92
- * @param {LH.Gatherer.Context} context
93
- * @return {Promise<LH.Artifacts['WebAppManifest']>}
94
- */
95
- async getArtifact(context) {
96
- const driver = context.driver;
97
- const {finalDisplayedUrl} = context.baseArtifacts.URL;
98
- try {
99
- return await WebAppManifest.getWebAppManifest(driver.defaultSession, finalDisplayedUrl);
100
- } catch {
101
- return null;
102
- }
103
- }
104
- }
105
-
106
- export default WebAppManifest;
@@ -1,55 +0,0 @@
1
- export class PwaCategoryRenderer extends CategoryRenderer {
2
- /**
3
- * Alters SVG id references so multiple instances of an SVG element can coexist
4
- * in a single page. If `svgRoot` has a `<defs>` block, gives all elements defined
5
- * in it unique ids, then updates id references (`<use xlink:href="...">`,
6
- * `fill="url(#...)"`) to the altered ids in all descendents of `svgRoot`.
7
- * @param {SVGElement} svgRoot
8
- */
9
- static _makeSvgReferencesUnique(svgRoot: SVGElement): void;
10
- /**
11
- * @param {LH.ReportResult.Category} category
12
- * @param {Object<string, LH.Result.ReportGroup>} [groupDefinitions]
13
- * @return {Element}
14
- */
15
- render(category: LH.ReportResult.Category, groupDefinitions?: {
16
- [x: string]: import("../../types/lhr/lhr.js").default.ReportGroup;
17
- } | undefined): Element;
18
- /**
19
- * @param {LH.ReportResult.Category} category
20
- * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
21
- * @return {DocumentFragment}
22
- */
23
- renderCategoryScore(category: LH.ReportResult.Category, groupDefinitions: Record<string, LH.Result.ReportGroup>): DocumentFragment;
24
- /**
25
- * Returns the group IDs found in auditRefs.
26
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
27
- * @return {!Set<string>}
28
- */
29
- _getGroupIds(auditRefs: Array<LH.ReportResult.AuditRef>): Set<string>;
30
- /**
31
- * Returns the group IDs whose audits are all considered passing.
32
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
33
- * @return {Set<string>}
34
- */
35
- _getPassingGroupIds(auditRefs: Array<LH.ReportResult.AuditRef>): Set<string>;
36
- /**
37
- * Returns a tooltip string summarizing group pass rates.
38
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
39
- * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
40
- * @return {string}
41
- */
42
- _getGaugeTooltip(auditRefs: Array<LH.ReportResult.AuditRef>, groupDefinitions: Record<string, LH.Result.ReportGroup>): string;
43
- /**
44
- * Render non-manual audits in groups, giving a badge to any group that has
45
- * all passing audits.
46
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
47
- * @param {Object<string, LH.Result.ReportGroup>} groupDefinitions
48
- * @return {Element}
49
- */
50
- _renderAudits(auditRefs: Array<LH.ReportResult.AuditRef>, groupDefinitions: {
51
- [x: string]: LH.Result.ReportGroup;
52
- }): Element;
53
- }
54
- import { CategoryRenderer } from './category-renderer.js';
55
- //# sourceMappingURL=pwa-category-renderer.d.ts.map
@@ -1,184 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2018 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
-
7
- import {CategoryRenderer} from './category-renderer.js';
8
- import {ReportUtils} from './report-utils.js';
9
- import {Globals} from './report-globals.js';
10
-
11
- export class PwaCategoryRenderer extends CategoryRenderer {
12
- /**
13
- * @param {LH.ReportResult.Category} category
14
- * @param {Object<string, LH.Result.ReportGroup>} [groupDefinitions]
15
- * @return {Element}
16
- */
17
- render(category, groupDefinitions = {}) {
18
- const categoryElem = this.dom.createElement('div', 'lh-category');
19
- categoryElem.id = category.id;
20
- // Deprecation warning banner.
21
- const pwaMessageContainer = this.dom.createComponent('warningsToplevel');
22
- const pwaMessageEl = this.dom.find('.lh-warnings__msg', pwaMessageContainer);
23
- pwaMessageEl.append(this.dom.convertMarkdownLinkSnippets(Globals.strings.pwaRemovalMessage));
24
-
25
- categoryElem.append(pwaMessageContainer);
26
- categoryElem.append(this.renderCategoryHeader(category, groupDefinitions));
27
-
28
- const auditRefs = category.auditRefs;
29
-
30
- // Regular audits aren't split up into pass/fail/notApplicable clumps, they're
31
- // all put in a top-level clump that isn't expandable/collapsible.
32
- const regularAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode !== 'manual');
33
- const auditsElem = this._renderAudits(regularAuditRefs, groupDefinitions);
34
- categoryElem.append(auditsElem);
35
-
36
- // Manual audits are still in a manual clump.
37
- const manualAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode === 'manual');
38
- const manualElem = this.renderClump('manual', {
39
- auditRefsOrEls: manualAuditRefs,
40
- description: category.manualDescription,
41
- openByDefault: true,
42
- });
43
- categoryElem.append(manualElem);
44
-
45
- return categoryElem;
46
- }
47
-
48
- /**
49
- * @param {LH.ReportResult.Category} category
50
- * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
51
- * @return {DocumentFragment}
52
- */
53
- renderCategoryScore(category, groupDefinitions) {
54
- // Defer to parent-gauge style if category error.
55
- if (category.score === null) {
56
- return super.renderScoreGauge(category, groupDefinitions);
57
- }
58
-
59
- const tmpl = this.dom.createComponent('gaugePwa');
60
- const wrapper = this.dom.find('a.lh-gauge--pwa__wrapper', tmpl);
61
-
62
- // Correct IDs in case multiple instances end up in the page.
63
- const svgRoot = tmpl.querySelector('svg');
64
- if (!svgRoot) throw new Error('no SVG element found in PWA score gauge template');
65
- PwaCategoryRenderer._makeSvgReferencesUnique(svgRoot);
66
-
67
- const allGroups = this._getGroupIds(category.auditRefs);
68
- const passingGroupIds = this._getPassingGroupIds(category.auditRefs);
69
-
70
- if (passingGroupIds.size === allGroups.size) {
71
- wrapper.classList.add('lh-badged--all');
72
- } else {
73
- for (const passingGroupId of passingGroupIds) {
74
- wrapper.classList.add(`lh-badged--${passingGroupId}`);
75
- }
76
- }
77
-
78
- this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
79
- wrapper.title = this._getGaugeTooltip(category.auditRefs, groupDefinitions);
80
- return tmpl;
81
- }
82
-
83
- /**
84
- * Returns the group IDs found in auditRefs.
85
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
86
- * @return {!Set<string>}
87
- */
88
- _getGroupIds(auditRefs) {
89
- const groupIds = auditRefs.map(ref => ref.group).filter(/** @return {g is string} */ g => !!g);
90
- return new Set(groupIds);
91
- }
92
-
93
- /**
94
- * Returns the group IDs whose audits are all considered passing.
95
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
96
- * @return {Set<string>}
97
- */
98
- _getPassingGroupIds(auditRefs) {
99
- const uniqueGroupIds = this._getGroupIds(auditRefs);
100
-
101
- // Remove any that have a failing audit.
102
- for (const auditRef of auditRefs) {
103
- if (!ReportUtils.showAsPassed(auditRef.result) && auditRef.group) {
104
- uniqueGroupIds.delete(auditRef.group);
105
- }
106
- }
107
-
108
- return uniqueGroupIds;
109
- }
110
-
111
- /**
112
- * Returns a tooltip string summarizing group pass rates.
113
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
114
- * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
115
- * @return {string}
116
- */
117
- _getGaugeTooltip(auditRefs, groupDefinitions) {
118
- const groupIds = this._getGroupIds(auditRefs);
119
-
120
- const tips = [];
121
- for (const groupId of groupIds) {
122
- const groupAuditRefs = auditRefs.filter(ref => ref.group === groupId);
123
- const auditCount = groupAuditRefs.length;
124
- const passedCount = groupAuditRefs.filter(ref => ReportUtils.showAsPassed(ref.result)).length;
125
-
126
- const title = groupDefinitions[groupId].title;
127
- tips.push(`${title}: ${passedCount}/${auditCount}`);
128
- }
129
-
130
- return tips.join(', ');
131
- }
132
-
133
- /**
134
- * Render non-manual audits in groups, giving a badge to any group that has
135
- * all passing audits.
136
- * @param {Array<LH.ReportResult.AuditRef>} auditRefs
137
- * @param {Object<string, LH.Result.ReportGroup>} groupDefinitions
138
- * @return {Element}
139
- */
140
- _renderAudits(auditRefs, groupDefinitions) {
141
- const auditsElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
142
-
143
- // Add a 'badged' class to group if all audits in that group pass.
144
- const passsingGroupIds = this._getPassingGroupIds(auditRefs);
145
- for (const groupId of passsingGroupIds) {
146
- const groupElem = this.dom.find(`.lh-audit-group--${groupId}`, auditsElem);
147
- groupElem.classList.add('lh-badged');
148
- }
149
-
150
- return auditsElem;
151
- }
152
-
153
- /**
154
- * Alters SVG id references so multiple instances of an SVG element can coexist
155
- * in a single page. If `svgRoot` has a `<defs>` block, gives all elements defined
156
- * in it unique ids, then updates id references (`<use xlink:href="...">`,
157
- * `fill="url(#...)"`) to the altered ids in all descendents of `svgRoot`.
158
- * @param {SVGElement} svgRoot
159
- */
160
- static _makeSvgReferencesUnique(svgRoot) {
161
- const defsEl = svgRoot.querySelector('defs');
162
- if (!defsEl) return;
163
-
164
- const idSuffix = Globals.getUniqueSuffix();
165
- const elementsToUpdate = defsEl.querySelectorAll('[id]');
166
- for (const el of elementsToUpdate) {
167
- const oldId = el.id;
168
- const newId = `${oldId}-${idSuffix}`;
169
- el.id = newId;
170
-
171
- // Update all <use>s.
172
- const useEls = svgRoot.querySelectorAll(`use[href="#${oldId}"]`);
173
- for (const useEl of useEls) {
174
- useEl.setAttribute('href', `#${newId}`);
175
- }
176
-
177
- // Update all fill="url(#...)"s.
178
- const fillEls = svgRoot.querySelectorAll(`[fill="url(#${oldId})"]`);
179
- for (const fillEl of fillEls) {
180
- fillEl.setAttribute('fill', `url(#${newId})`);
181
- }
182
- }
183
- }
184
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2020 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- 'use strict';
7
-
8
- import fetch from 'node-fetch';
9
-
10
- import {UIStrings} from '../../core/audits/installable-manifest.js';
11
-
12
- describe('installabilityErrors', () => {
13
- let chromiumErrorIds;
14
-
15
- before(async () => {
16
- const installableLoggingGitTilesUrl =
17
- 'https://chromium.googlesource.com/chromium/src/+/main/components/webapps/browser/installable/installable_logging.cc?format=TEXT';
18
- const resp = await fetch(installableLoggingGitTilesUrl);
19
- if (!resp.ok) {
20
- throw new Error(`Chromium source fetch failed: ${resp.status} ${resp.statusText}`);
21
- }
22
- const base64 = await resp.text();
23
- const buff = Buffer.from(base64, 'base64');
24
- const text = buff.toString('utf-8');
25
-
26
- // Apply some *beautiful* regexes to parse the C++ of https://chromium.googlesource.com/chromium/src/+/main/chrome/browser/installable/installable_logging.cc
27
- const handledErrorConsts = [...text.matchAll(/error_id = (k.*?);/g)].map(([_, match]) => match);
28
- const errorConstsToIdsCode = [...text.matchAll(/static const char k.*?Id\[\](.|\n)*?;/g)].map(
29
- ([match]) => match.replace(/\n/, '')
30
- );
31
- const errorConstsToIds = errorConstsToIdsCode.map(txt => [
32
- txt.match(/char (k.*?)\[/)[1],
33
- txt.match(/ "(.*?)"/)[1],
34
- ]);
35
- const errorConstsToIdsDict = Object.fromEntries(errorConstsToIds);
36
-
37
- chromiumErrorIds = handledErrorConsts.map(varName => {
38
- if (!errorConstsToIdsDict[varName]) throw new Error(`Error const (${varName}) not found!`);
39
- return errorConstsToIdsDict[varName];
40
- }).sort();
41
- });
42
-
43
- it('should notify us if something changed', () => {
44
- expect(chromiumErrorIds).toMatchInlineSnapshot(`
45
- Array [
46
- "already-installed",
47
- "cannot-download-icon",
48
- "ids-do-not-match",
49
- "in-incognito",
50
- "manifest-display-not-supported",
51
- "manifest-display-override-not-supported",
52
- "manifest-location-changed",
53
- "manifest-missing-name-or-short-name",
54
- "manifest-missing-suitable-icon",
55
- "manifest-parsing-or-network-error",
56
- "no-acceptable-icon",
57
- "no-icon-available",
58
- "no-id-specified",
59
- "no-manifest",
60
- "no-url-for-service-worker",
61
- "not-from-secure-origin",
62
- "not-offline-capable",
63
- "pipeline-restarted",
64
- "platform-not-supported-on-android",
65
- "prefer-related-applications",
66
- "prefer-related-applications-only-beta-stable",
67
- "start-url-not-valid",
68
- "url-not-supported-for-webapk",
69
- ]
70
- `);
71
- });
72
-
73
- it('are each handled explicitly in the gatherer', () => {
74
- const errorStrings = Object.keys(UIStrings)
75
- .filter(key => chromiumErrorIds.includes(key))
76
- .sort();
77
- expect(errorStrings).toEqual(chromiumErrorIds);
78
- });
79
- });