lighthouse 8.5.1 → 8.6.0-dev.20211016

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 (173) hide show
  1. package/changelog.md +5490 -0
  2. package/dist/report/flow.js +35 -12
  3. package/dist/report/standalone.js +20 -21
  4. package/flow-report/.eslintrc.cjs +49 -0
  5. package/flow-report/assets/styles.css +272 -9
  6. package/flow-report/jest.config.js +1 -1
  7. package/flow-report/package.json +4 -0
  8. package/flow-report/src/app.tsx +31 -9
  9. package/flow-report/src/common.tsx +86 -7
  10. package/flow-report/src/header.tsx +88 -0
  11. package/flow-report/src/help-dialog.tsx +120 -0
  12. package/flow-report/src/i18n/i18n.tsx +52 -0
  13. package/flow-report/src/i18n/localized-strings.js +11 -0
  14. package/flow-report/src/i18n/ui-strings.js +134 -0
  15. package/flow-report/src/icons.tsx +20 -3
  16. package/flow-report/src/sidebar/sidebar.tsx +11 -16
  17. package/flow-report/src/summary/category.tsx +39 -24
  18. package/flow-report/src/summary/summary.tsx +35 -32
  19. package/flow-report/src/topbar.tsx +54 -4
  20. package/flow-report/src/util.ts +26 -1
  21. package/flow-report/src/wrappers/report-renderer.tsx +1 -1
  22. package/flow-report/src/wrappers/report.tsx +6 -21
  23. package/flow-report/test/app-test.tsx +17 -13
  24. package/flow-report/test/common-test.tsx +89 -0
  25. package/flow-report/test/header-test.tsx +55 -0
  26. package/flow-report/test/sample-flow.ts +17 -0
  27. package/flow-report/test/setup/env-setup.ts +1 -0
  28. package/flow-report/test/sidebar/flow-test.tsx +6 -12
  29. package/flow-report/test/sidebar/sidebar-test.tsx +7 -13
  30. package/flow-report/test/summary/category-test.tsx +83 -34
  31. package/flow-report/test/summary/summary-test.tsx +12 -18
  32. package/flow-report/test/topbar-test.tsx +68 -0
  33. package/flow-report/test/util-test.tsx +1 -12
  34. package/flow-report/tsconfig.json +2 -0
  35. package/flow-report/types/flow-report.d.ts +2 -1
  36. package/jest.config.js +1 -15
  37. package/lighthouse-cli/bin.js +5 -0
  38. package/lighthouse-cli/cli-flags.js +10 -3
  39. package/lighthouse-cli/commands/commands.js +1 -0
  40. package/lighthouse-cli/commands/list-locales.js +16 -0
  41. package/lighthouse-cli/run.js +2 -2
  42. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +0 -0
  43. package/lighthouse-cli/test/smokehouse/report-assert.js +20 -4
  44. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -1
  45. package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +1 -1
  46. package/lighthouse-core/audits/seo/crawlable-anchors.js +10 -12
  47. package/lighthouse-core/computed/js-bundles.js +1 -2
  48. package/lighthouse-core/computed/unused-javascript-summary.js +3 -3
  49. package/lighthouse-core/config/config-helpers.js +1 -0
  50. package/lighthouse-core/config/config.js +2 -2
  51. package/lighthouse-core/config/default-config.js +5 -0
  52. package/lighthouse-core/fraggle-rock/api.js +10 -0
  53. package/lighthouse-core/fraggle-rock/config/filters.js +22 -3
  54. package/lighthouse-core/fraggle-rock/gather/driver.js +4 -0
  55. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +2 -2
  56. package/lighthouse-core/fraggle-rock/user-flow.js +35 -6
  57. package/lighthouse-core/gather/driver/navigation.js +5 -0
  58. package/lighthouse-core/gather/driver/prepare.js +14 -0
  59. package/lighthouse-core/gather/driver/storage.js +5 -0
  60. package/lighthouse-core/gather/gather-runner.js +2 -2
  61. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +5 -1
  62. package/lighthouse-core/gather/gatherers/full-page-screenshot.js +30 -24
  63. package/lighthouse-core/lib/cdt/Platform.js +55 -0
  64. package/lighthouse-core/lib/cdt/SDK.js +3 -123
  65. package/lighthouse-core/lib/cdt/generated/SourceMap.js +148 -312
  66. package/lighthouse-core/lib/csp-evaluator.js +2 -1
  67. package/lighthouse-core/lib/i18n/README.md +6 -6
  68. package/lighthouse-core/lib/i18n/i18n.js +26 -348
  69. package/lighthouse-core/lib/page-functions.js +4 -4
  70. package/lighthouse-core/lib/stack-packs.js +1 -13
  71. package/lighthouse-core/runner.js +6 -6
  72. package/lighthouse-core/scripts/manual-chrome-launcher.js +4 -1
  73. package/lighthouse-core/scripts/package.json +4 -0
  74. package/lighthouse-core/util-commonjs.js +20 -4
  75. package/package.json +19 -16
  76. package/readme.md +1 -1
  77. package/report/assets/standalone-template.html +0 -1
  78. package/report/assets/styles.css +16 -41
  79. package/report/assets/templates.html +42 -4
  80. package/report/clients/psi.js +1 -0
  81. package/report/clients/standalone.js +1 -2
  82. package/report/generator/file-namer.js +17 -8
  83. package/report/generator/report-generator.js +0 -1
  84. package/report/renderer/category-renderer.js +4 -16
  85. package/report/renderer/components.js +107 -71
  86. package/report/renderer/dom.js +19 -0
  87. package/report/renderer/logger.js +35 -2
  88. package/report/renderer/performance-category-renderer.js +23 -21
  89. package/report/renderer/pwa-category-renderer.js +1 -2
  90. package/report/renderer/report-renderer.js +21 -0
  91. package/report/renderer/report-ui-features.js +7 -19
  92. package/report/renderer/swap-locale-feature.js +82 -0
  93. package/report/renderer/util.js +20 -4
  94. package/report/test/clients/psi-test.js +0 -4
  95. package/report/test/generator/file-namer-test.js +2 -2
  96. package/report/test/renderer/category-renderer-test.js +1 -1
  97. package/report/test/renderer/performance-category-renderer-test.js +12 -0
  98. package/report/test/renderer/report-renderer-axe-test.js +86 -0
  99. package/report/test/renderer/report-renderer-test.js +0 -55
  100. package/report/test/renderer/util-test.js +68 -8
  101. package/report/test-assets/faux-psi-template.html +0 -1
  102. package/report/tsconfig.json +2 -1
  103. package/report/types/html-renderer.d.ts +2 -1
  104. package/root.js +18 -0
  105. package/shared/localization/format.js +455 -0
  106. package/{lighthouse-core/lib/cdt/Common.js → shared/localization/i18n-module.js} +3 -8
  107. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ar-XB.json +139 -4
  108. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ar.json +141 -6
  109. package/{lighthouse-core/lib/i18n → shared/localization}/locales/bg.json +139 -4
  110. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ca.json +140 -5
  111. package/{lighthouse-core/lib/i18n → shared/localization}/locales/cs.json +139 -4
  112. package/{lighthouse-core/lib/i18n → shared/localization}/locales/da.json +139 -4
  113. package/{lighthouse-core/lib/i18n → shared/localization}/locales/de.json +139 -4
  114. package/{lighthouse-core/lib/i18n → shared/localization}/locales/el.json +141 -6
  115. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-GB.json +139 -4
  116. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-US.json +129 -0
  117. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-XA.json +139 -4
  118. package/{lighthouse-core/lib/i18n → shared/localization}/locales/en-XL.json +129 -0
  119. package/{lighthouse-core/lib/i18n → shared/localization}/locales/es-419.json +139 -4
  120. package/{lighthouse-core/lib/i18n → shared/localization}/locales/es.json +139 -4
  121. package/{lighthouse-core/lib/i18n → shared/localization}/locales/fi.json +139 -4
  122. package/{lighthouse-core/lib/i18n → shared/localization}/locales/fil.json +139 -4
  123. package/{lighthouse-core/lib/i18n → shared/localization}/locales/fr.json +139 -4
  124. package/{lighthouse-core/lib/i18n → shared/localization}/locales/he.json +139 -4
  125. package/{lighthouse-core/lib/i18n → shared/localization}/locales/hi.json +142 -7
  126. package/{lighthouse-core/lib/i18n → shared/localization}/locales/hr.json +139 -4
  127. package/{lighthouse-core/lib/i18n → shared/localization}/locales/hu.json +139 -4
  128. package/{lighthouse-core/lib/i18n → shared/localization}/locales/id.json +139 -4
  129. package/{lighthouse-core/lib/i18n → shared/localization}/locales/it.json +139 -4
  130. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ja.json +139 -4
  131. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ko.json +139 -4
  132. package/{lighthouse-core/lib/i18n → shared/localization}/locales/lt.json +139 -4
  133. package/{lighthouse-core/lib/i18n → shared/localization}/locales/lv.json +139 -4
  134. package/{lighthouse-core/lib/i18n → shared/localization}/locales/nl.json +139 -4
  135. package/{lighthouse-core/lib/i18n → shared/localization}/locales/no.json +139 -4
  136. package/{lighthouse-core/lib/i18n → shared/localization}/locales/pl.json +139 -4
  137. package/{lighthouse-core/lib/i18n → shared/localization}/locales/pt-PT.json +139 -4
  138. package/{lighthouse-core/lib/i18n → shared/localization}/locales/pt.json +139 -4
  139. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ro.json +139 -4
  140. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ru.json +139 -4
  141. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sk.json +139 -4
  142. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sl.json +139 -4
  143. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sr-Latn.json +139 -4
  144. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sr.json +139 -4
  145. package/{lighthouse-core/lib/i18n → shared/localization}/locales/sv.json +139 -4
  146. package/{lighthouse-core/lib/i18n → shared/localization}/locales/ta.json +146 -11
  147. package/{lighthouse-core/lib/i18n → shared/localization}/locales/te.json +179 -44
  148. package/{lighthouse-core/lib/i18n → shared/localization}/locales/th.json +139 -4
  149. package/{lighthouse-core/lib/i18n → shared/localization}/locales/tr.json +139 -4
  150. package/{lighthouse-core/lib/i18n → shared/localization}/locales/uk.json +139 -4
  151. package/{lighthouse-core/lib/i18n → shared/localization}/locales/vi.json +139 -4
  152. package/{lighthouse-core/lib/i18n → shared/localization}/locales/zh-HK.json +139 -4
  153. package/{lighthouse-core/lib/i18n → shared/localization}/locales/zh-TW.json +139 -4
  154. package/{lighthouse-core/lib/i18n → shared/localization}/locales/zh.json +139 -4
  155. package/{lighthouse-core/lib/i18n → shared/localization}/locales.js +8 -1
  156. package/shared/localization/swap-flow-locale.js +20 -0
  157. package/{lighthouse-core/lib/i18n → shared/localization}/swap-locale.js +9 -7
  158. package/shared/test/localization/format-test.js +421 -0
  159. package/shared/test/localization/locales-test.js +47 -0
  160. package/shared/test/localization/swap-locale-test.js +140 -0
  161. package/shared/tsconfig.json +22 -0
  162. package/{lighthouse-core/lib → shared}/type-verifiers.js +0 -0
  163. package/shared/types/shared.d.ts +24 -0
  164. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +5 -3
  165. package/third-party/chromium-synchronization/installability-errors-test.js +2 -1
  166. package/tsconfig-all.json +1 -0
  167. package/tsconfig-base.json +1 -1
  168. package/tsconfig.json +2 -3
  169. package/types/config.d.ts +1 -0
  170. package/types/es-main.d.ts +18 -0
  171. package/types/externs.d.ts +2 -0
  172. package/types/lhr/flow.d.ts +7 -0
  173. package/types/lhr/i18n.d.ts +0 -3
@@ -0,0 +1,421 @@
1
+ /**
2
+ * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ const path = require('path');
9
+
10
+ const format = require('../../localization/format.js');
11
+ const i18n = require('../../../lighthouse-core/lib/i18n/i18n.js');
12
+ const constants = require('../../../lighthouse-core/config/constants.js');
13
+ const locales = require('../../localization/locales.js');
14
+
15
+ /* eslint-env jest */
16
+
17
+ describe('format', () => {
18
+ describe('DEFAULT_LOCALE', () => {
19
+ it('is the same as the default config locale', () => {
20
+ expect(format.DEFAULT_LOCALE).toBe(constants.defaultSettings.locale);
21
+ });
22
+ });
23
+
24
+ describe('#getAvailableLocales', () => {
25
+ it('has all the available locales', () => {
26
+ const availableLocales = format.getAvailableLocales();
27
+ for (const locale of ['en', 'es', 'ru', 'zh']) {
28
+ expect(availableLocales).toContain(locale);
29
+ }
30
+
31
+ const rawLocales = Object.keys(locales).sort();
32
+ expect(availableLocales.sort()).toEqual(rawLocales);
33
+ });
34
+
35
+ it('contains the default locale', () => {
36
+ expect(format.getAvailableLocales()).toContain(format.DEFAULT_LOCALE);
37
+ });
38
+ });
39
+
40
+ describe('#getCanonicalLocales', () => {
41
+ it('contains some canonical locales', () => {
42
+ const canonicalLocales = format.getCanonicalLocales();
43
+ for (const locale of ['en-US', 'es', 'ru', 'zh']) {
44
+ expect(canonicalLocales).toContain(locale);
45
+ }
46
+ });
47
+
48
+ it('is a subset of the available locales', () => {
49
+ const canonicalLocales = format.getCanonicalLocales();
50
+ const availableLocales = format.getAvailableLocales();
51
+
52
+ for (const canonicalLocale of canonicalLocales) {
53
+ expect(availableLocales).toContain(canonicalLocale);
54
+ }
55
+
56
+ expect(canonicalLocales.length).toBeLessThan(availableLocales.length);
57
+ });
58
+ });
59
+
60
+ describe('#_formatPathAsString', () => {
61
+ it('handles simple paths', () => {
62
+ expect(format._formatPathAsString(['foo'])).toBe('foo');
63
+ expect(format._formatPathAsString(['foo', 'bar', 'baz'])).toBe('foo.bar.baz');
64
+ });
65
+
66
+ it('handles array paths', () => {
67
+ expect(format._formatPathAsString(['foo', 0])).toBe('foo[0]');
68
+ });
69
+
70
+ it('handles complex paths', () => {
71
+ const propertyPath = ['foo', 'what-the', 'bar', 0, 'no'];
72
+ expect(format._formatPathAsString(propertyPath)).toBe('foo[what-the].bar[0].no');
73
+ });
74
+
75
+ it('throws on unhandleable paths', () => {
76
+ expect(() => format._formatPathAsString(['Bobby "DROP TABLE'])).toThrow(/Cannot handle/);
77
+ });
78
+ });
79
+
80
+ describe('#replaceIcuMessages', () => {
81
+ it('replaces the references in the LHR', () => {
82
+ const fakeFile = path.join(__dirname, 'fake-file-number-2.js');
83
+ const UIStrings = {aString: 'different {x}!'};
84
+ const formatter = i18n.createMessageInstanceIdFn(fakeFile, UIStrings);
85
+
86
+ const title = formatter(UIStrings.aString, {x: 1});
87
+ const lhr = {audits: {'fake-audit': {title}}};
88
+
89
+ const icuMessagePaths = format.replaceIcuMessages(lhr, 'en-US');
90
+ expect(lhr.audits['fake-audit'].title).toBe('different 1!');
91
+
92
+ const expectedPathId = 'shared/test/localization/fake-file-number-2.js | aString';
93
+ expect(icuMessagePaths).toEqual({
94
+ [expectedPathId]: [{path: 'audits[fake-audit].title', values: {x: 1}}]});
95
+ });
96
+ });
97
+
98
+ describe('#getRendererFormattedStrings', () => {
99
+ it('returns icu messages in the specified locale', () => {
100
+ const strings = format.getRendererFormattedStrings('en-XA');
101
+ expect(strings.passedAuditsGroupTitle).toEqual('[Þåššéð åûðîţš one two]');
102
+ expect(strings.snippetCollapseButtonLabel).toEqual('[Çöļļåþšé šñîþþéţ one two]');
103
+ });
104
+
105
+ it('throws an error for invalid locales', () => {
106
+ expect(_ => format.getRendererFormattedStrings('not-a-locale'))
107
+ .toThrow(`Unsupported locale 'not-a-locale'`);
108
+ });
109
+ });
110
+
111
+ describe('#getFormatted', () => {
112
+ it('returns the formatted string', () => {
113
+ const UIStrings = {testMessage: 'happy test'};
114
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
115
+ const formattedStr = format.getFormatted(str_(UIStrings.testMessage), 'en');
116
+ expect(formattedStr).toEqual('happy test');
117
+ });
118
+
119
+ it('returns the formatted string with replacements', () => {
120
+ const UIStrings = {testMessage: 'replacement test ({errorCode})'};
121
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
122
+ const formattedStr = format.getFormatted(str_(UIStrings.testMessage,
123
+ {errorCode: 'BOO'}), 'en');
124
+ expect(formattedStr).toEqual('replacement test (BOO)');
125
+ });
126
+
127
+ it('throws an error for invalid locales', () => {
128
+ // Populate a string to try to localize to a bad locale.
129
+ const UIStrings = {testMessage: 'testy test'};
130
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
131
+
132
+ expect(_ => format.getFormatted(str_(UIStrings.testMessage), 'still-not-a-locale'))
133
+ .toThrow(`Unsupported locale 'still-not-a-locale'`);
134
+ });
135
+
136
+ it('does not alter the passed-in replacement values object', () => {
137
+ const UIStrings = {
138
+ testMessage: 'needs {count, number, bytes}KB test {str} in {timeInMs, number, seconds}s',
139
+ };
140
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
141
+
142
+ const replacements = {
143
+ count: 2555,
144
+ str: '*units*',
145
+ timeInMs: 314159265,
146
+ };
147
+ const replacementsClone = JSON.parse(JSON.stringify(replacements));
148
+
149
+ const formattedStr = format.getFormatted(str_(UIStrings.testMessage, replacements), 'en');
150
+ expect(formattedStr).toEqual('needs 2KB test *units* in 314,159.3s');
151
+
152
+ expect(replacements).toEqual(replacementsClone);
153
+ });
154
+
155
+ it('returns a message that is already a string unchanged', () => {
156
+ const testString = 'kind of looks like it needs ({formatting})';
157
+ const formattedStr = format.getFormatted(testString, 'pl');
158
+ expect(formattedStr).toBe(testString);
159
+ });
160
+
161
+ it('throws an error if formatting something other than IcuMessages or strings', () => {
162
+ expect(_ => format.getFormatted(15, 'lt'))
163
+ .toThrow(`Attempted to format invalid icuMessage type`);
164
+ expect(_ => format.getFormatted(new Date(), 'sr-Latn'))
165
+ .toThrow(`Attempted to format invalid icuMessage type`);
166
+ });
167
+ });
168
+
169
+ describe('#registerLocaleData', () => {
170
+ // Store original locale data so we can restore at the end
171
+ const moduleLocales = require('../../localization/locales.js');
172
+ const clonedLocales = JSON.parse(JSON.stringify(moduleLocales));
173
+
174
+ it('installs new locale strings', () => {
175
+ const localeData = {
176
+ 'shared/test/localization/format-test.js | testString': {
177
+ 'message': 'en-XZ cuerda!',
178
+ },
179
+ };
180
+ format.registerLocaleData('en-XZ', localeData);
181
+
182
+ const UIStrings = {testString: 'en-US string!'};
183
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
184
+ const formattedStr = format.getFormatted(str_(UIStrings.testString), 'en-XZ');
185
+ expect(formattedStr).toEqual('en-XZ cuerda!');
186
+ });
187
+
188
+ it('overwrites existing locale strings', () => {
189
+ const filename = 'lighthouse-core/audits/is-on-https.js';
190
+ const UIStrings = require('../../../lighthouse-core/audits/is-on-https.js').UIStrings;
191
+ const str_ = i18n.createMessageInstanceIdFn(filename, UIStrings);
192
+
193
+ // To start with, we get back the intended string..
194
+ const origTitle = format.getFormatted(str_(UIStrings.title), 'es-419');
195
+ expect(origTitle).toEqual('Usa HTTPS');
196
+ const origFailureTitle = format.getFormatted(str_(UIStrings.failureTitle), 'es-419');
197
+ expect(origFailureTitle).toEqual('No usa HTTPS');
198
+
199
+ // Now we declare and register the new string...
200
+ const localeData = {
201
+ 'lighthouse-core/audits/is-on-https.js | title': {
202
+ 'message': 'new string for es-419 uses https!',
203
+ },
204
+ };
205
+ format.registerLocaleData('es-419', localeData);
206
+
207
+ // And confirm that's what is returned
208
+ const newTitle = format.getFormatted(str_(UIStrings.title), 'es-419');
209
+ expect(newTitle).toEqual('new string for es-419 uses https!');
210
+
211
+ // Meanwhile another string that wasn't set in registerLocaleData just falls back to english
212
+ const newFailureTitle = format.getFormatted(str_(UIStrings.failureTitle), 'es-419');
213
+ expect(newFailureTitle).toEqual('Does not use HTTPS');
214
+
215
+ // Restore overwritten strings to avoid messing with other tests
216
+ moduleLocales['es-419'] = clonedLocales['es-419'];
217
+ const title = format.getFormatted(str_(UIStrings.title), 'es-419');
218
+ expect(title).toEqual('Usa HTTPS');
219
+ });
220
+ });
221
+
222
+ describe('#isIcuMessage', () => {
223
+ const icuMessage = {
224
+ i18nId: 'shared/test/localization/fake-file.js | title',
225
+ values: {x: 1},
226
+ formattedDefault: 'a default',
227
+ };
228
+
229
+ it('passes a valid LH.IcuMessage', () => {
230
+ expect(format.isIcuMessage(icuMessage)).toBe(true);
231
+ });
232
+
233
+ it('fails non-objects', () => {
234
+ expect(format.isIcuMessage(undefined)).toBe(false);
235
+ expect(format.isIcuMessage(null)).toBe(false);
236
+ expect(format.isIcuMessage('ICU!')).toBe(false);
237
+ expect(format.isIcuMessage(55)).toBe(false);
238
+ expect(format.isIcuMessage([
239
+ icuMessage,
240
+ icuMessage,
241
+ ])).toBe(false);
242
+ });
243
+
244
+ it('fails invalid or missing i18nIds', () => {
245
+ const badIdMessage = {...icuMessage, i18nId: 0};
246
+ expect(format.isIcuMessage(badIdMessage)).toBe(false);
247
+
248
+ const noIdMessage = {...icuMessage};
249
+ delete noIdMessage.i18nId;
250
+ expect(format.isIcuMessage(noIdMessage)).toBe(false);
251
+ });
252
+
253
+ it('fails invalid or missing formattedDefault', () => {
254
+ const badDefaultMessage = {...icuMessage, formattedDefault: -0};
255
+ expect(format.isIcuMessage(badDefaultMessage)).toBe(false);
256
+
257
+ const noDefaultMessage = {...icuMessage};
258
+ delete noDefaultMessage.formattedDefault;
259
+ expect(format.isIcuMessage(noDefaultMessage)).toBe(false);
260
+ });
261
+
262
+ it('passes missing values', () => {
263
+ const emptyValuesMessage = {...icuMessage, values: {}};
264
+ expect(format.isIcuMessage(emptyValuesMessage)).toBe(true);
265
+
266
+ const noValuesMessage = {...icuMessage};
267
+ delete noValuesMessage.values;
268
+ expect(format.isIcuMessage(noValuesMessage)).toBe(true);
269
+ });
270
+
271
+ it('fails invalid values types', () => {
272
+ const badValuesMessage = {...icuMessage, values: NaN};
273
+ expect(format.isIcuMessage(badValuesMessage)).toBe(false);
274
+ const nullValuesMessage = {...icuMessage, values: null};
275
+ expect(format.isIcuMessage(nullValuesMessage)).toBe(false);
276
+ });
277
+
278
+ it(`fails invalid values' values types`, () => {
279
+ const badValuesValuesMessage = {...icuMessage, values: {a: false}};
280
+ expect(format.isIcuMessage(badValuesValuesMessage)).toBe(false);
281
+ });
282
+ });
283
+
284
+ describe('#getIcuMessageIdParts', () => {
285
+ it('returns valid ICU message id parts', () => {
286
+ const {filename, key} = format.getIcuMessageIdParts('path/to/file.js | modeName');
287
+ expect(filename).toEqual('path/to/file.js');
288
+ expect(key).toEqual('modeName');
289
+ });
290
+
291
+ it('throws on invalid ICU message id', () => {
292
+ expect(() => {
293
+ format.getIcuMessageIdParts('path/to/file.js');
294
+ }).toThrow();
295
+ });
296
+ });
297
+
298
+ describe('Message values are properly formatted', () => {
299
+ // Message strings won't be in locale files, so will fall back to values given here.
300
+ const UIStrings = {
301
+ helloWorld: 'Hello World',
302
+ helloBytesWorld: 'Hello {in, number, bytes} World',
303
+ helloMsWorld: 'Hello {in, number, milliseconds} World',
304
+ helloSecWorld: 'Hello {in, number, seconds} World',
305
+ helloTimeInMsWorld: 'Hello {timeInMs, number, seconds} World',
306
+ helloPercentWorld: 'Hello {in, number, extendedPercent} World',
307
+ helloWorldMultiReplace: '{hello} {world}',
308
+ helloPlural: '{itemCount, plural, =1{1 hello} other{hellos}}',
309
+ helloPluralNestedICU: '{itemCount, plural, ' +
310
+ '=1{1 hello {in, number, bytes}} ' +
311
+ 'other{hellos {in, number, bytes}}}',
312
+ helloPluralNestedPluralAndICU: '{itemCount, plural, ' +
313
+ '=1{{innerItemCount, plural, ' +
314
+ '=1{1 hello 1 goodbye {in, number, bytes}} ' +
315
+ 'other{1 hello, goodbyes {in, number, bytes}}}} ' +
316
+ 'other{{innerItemCount, plural, ' +
317
+ '=1{hellos 1 goodbye {in, number, bytes}} ' +
318
+ 'other{hellos, goodbyes {in, number, bytes}}}}}',
319
+ };
320
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
321
+
322
+ it('formats a basic message', () => {
323
+ const helloStr = str_(UIStrings.helloWorld);
324
+ expect(helloStr).toBeDisplayString('Hello World');
325
+ });
326
+
327
+ it('formats a message with bytes', () => {
328
+ const helloBytesStr = str_(UIStrings.helloBytesWorld, {in: 1875});
329
+ expect(helloBytesStr).toBeDisplayString('Hello 2 World');
330
+ });
331
+
332
+ it('formats a message with milliseconds', () => {
333
+ const helloMsStr = str_(UIStrings.helloMsWorld, {in: 432});
334
+ expect(helloMsStr).toBeDisplayString('Hello 430 World');
335
+ });
336
+
337
+ it('formats a message with seconds', () => {
338
+ const helloSecStr = str_(UIStrings.helloSecWorld, {in: 753});
339
+ expect(helloSecStr).toBeDisplayString('Hello 753.0 World');
340
+ });
341
+
342
+ it('formats a message with seconds timeInMs', () => {
343
+ const helloTimeInMsStr = str_(UIStrings.helloTimeInMsWorld, {timeInMs: 753543});
344
+ expect(helloTimeInMsStr).toBeDisplayString('Hello 753.5 World');
345
+ });
346
+
347
+ it('formats a message with extended percent', () => {
348
+ const helloPercentStr = str_(UIStrings.helloPercentWorld, {in: 0.43078});
349
+ expect(helloPercentStr).toBeDisplayString('Hello 43.08% World');
350
+ });
351
+
352
+ it('throws an error when values are needed but not provided', () => {
353
+ expect(_ => format.getFormatted(str_(UIStrings.helloBytesWorld), 'en-US'))
354
+ // eslint-disable-next-line max-len
355
+ .toThrow(`ICU Message "Hello {in, number, bytes} World" contains a value reference ("in") that wasn't provided`);
356
+ });
357
+
358
+ it('throws an error when a value is missing', () => {
359
+ expect(_ => format.getFormatted(str_(UIStrings.helloWorldMultiReplace,
360
+ {hello: 'hello'}), 'en-US'))
361
+ // eslint-disable-next-line max-len
362
+ .toThrow(`ICU Message "{hello} {world}" contains a value reference ("world") that wasn't provided`);
363
+ });
364
+
365
+ it('formats a message with plurals', () => {
366
+ const helloStr = str_(UIStrings.helloPlural, {itemCount: 3});
367
+ expect(helloStr).toBeDisplayString('hellos');
368
+ });
369
+
370
+ it('throws an error when a plural control value is missing', () => {
371
+ expect(_ => i18n.getFormatted(str_(UIStrings.helloPlural), 'en-US'))
372
+ // eslint-disable-next-line max-len
373
+ .toThrow(`ICU Message "{itemCount, plural, =1{1 hello} other{hellos}}" contains a value reference ("itemCount") that wasn't provided`);
374
+ });
375
+
376
+ it('formats a message with plurals and nested custom ICU', () => {
377
+ const helloStr = str_(UIStrings.helloPluralNestedICU, {itemCount: 3, in: 1875});
378
+ expect(helloStr).toBeDisplayString('hellos 2');
379
+ });
380
+
381
+ it('formats a message with plurals and nested custom ICU and nested plural', () => {
382
+ const helloStr = str_(UIStrings.helloPluralNestedPluralAndICU, {itemCount: 3,
383
+ innerItemCount: 1,
384
+ in: 1875});
385
+ expect(helloStr).toBeDisplayString('hellos 1 goodbye 2');
386
+ });
387
+
388
+ it('throws an error if a string value is used for a numeric placeholder', () => {
389
+ expect(_ => str_(UIStrings.helloTimeInMsWorld, {
390
+ timeInMs: 'string not a number',
391
+ }))
392
+ // eslint-disable-next-line max-len
393
+ .toThrow(`ICU Message "Hello {timeInMs, number, seconds} World" contains a numeric reference ("timeInMs") but provided value was not a number`);
394
+ });
395
+
396
+ it('throws an error if a value is provided that has no placeholder in the message', () => {
397
+ expect(_ => str_(UIStrings.helloTimeInMsWorld, {
398
+ timeInMs: 55,
399
+ sirNotAppearingInThisString: 66,
400
+ }))
401
+ // eslint-disable-next-line max-len
402
+ .toThrow(`Provided value "sirNotAppearingInThisString" does not match any placeholder in ICU message "Hello {timeInMs, number, seconds} World"`);
403
+ });
404
+
405
+ it('formats correctly with NaN and Infinity numeric values', () => {
406
+ const helloInfinityStr = str_(UIStrings.helloBytesWorld, {in: Infinity});
407
+ expect(helloInfinityStr).toBeDisplayString('Hello ∞ World');
408
+
409
+ const helloNaNStr = str_(UIStrings.helloBytesWorld, {in: NaN});
410
+ // TODO(COMPAT): workaround can be removed after Node 13 is retired.
411
+ // expect(helloNaNStr).toBeDisplayString('Hello NaN World');
412
+
413
+ // Node 13/V8 7.9 and 8.0 have a bug where `({a: NaN}).a.toLocaleString() === "-NaN"`. It
414
+ // works correctly in Node 12 and 14, so work around it since NaN isn't essential for
415
+ // user-facing strings and it will eventually correct itself.
416
+ const formattedNaNStr = format.getFormatted(helloNaNStr, 'en-US');
417
+ expect(formattedNaNStr === 'Hello NaN World' || formattedNaNStr === 'Hello -NaN World')
418
+ .toBe(true);
419
+ });
420
+ });
421
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ const locales = require('../../localization/locales.js');
9
+ const assert = require('assert').strict;
10
+
11
+ /* eslint-env jest */
12
+
13
+ describe('locales', () => {
14
+ it('has only canonical (or expected-deprecated) language tags', () => {
15
+ // Map of deprecated codes to their canonical version. Depending on the ICU
16
+ // version used to run Lighthouse/this test, these *may* come back as their
17
+ // substitute, not themselves.
18
+ const deprecatedCodes = {
19
+ in: 'id',
20
+ iw: 'he',
21
+ mo: 'ro',
22
+ tl: 'fil',
23
+ };
24
+
25
+ for (const locale of Object.keys(locales)) {
26
+ const canonicalLocale = Intl.getCanonicalLocales(locale)[0];
27
+ const substitute = deprecatedCodes[locale];
28
+ assert.ok(locale === canonicalLocale || substitute === canonicalLocale,
29
+ `locale code '${locale}' not canonical ('${canonicalLocale}' found instead)`);
30
+ }
31
+
32
+ // Deprecation subsitutes should be removed from the test if no longer used.
33
+ for (const locale of Object.keys(deprecatedCodes)) {
34
+ assert.ok(locales[locale], `${locale} substitute should be removed from test`);
35
+ }
36
+ });
37
+
38
+ it('has a base language prefix fallback for all supported languages', () => {
39
+ for (const locale of Object.keys(locales)) {
40
+ const basePrefix = locale.split('-')[0];
41
+ // The internet sez there is no canonical Chinese, so we exclude that one.
42
+ if (basePrefix !== 'zh') {
43
+ assert.ok(locales[basePrefix], `${locale} is missing a base fallback`);
44
+ }
45
+ }
46
+ });
47
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ const swapLocale = require('../../localization/swap-locale.js');
9
+ const {isNode12SmallIcu} = require('../../../lighthouse-core/test/test-utils.js');
10
+
11
+ const lhr = require('../../../lighthouse-core/test/results/sample_v2.json');
12
+
13
+ /* eslint-env jest */
14
+ describe('swap-locale', () => {
15
+ // COMPAT: Node 12 only has 'en' by default. Skip these tests since they're all about swapping locales.
16
+ if (isNode12SmallIcu()) {
17
+ // Jest requires at least one test per suite.
18
+ it('throws if locale is not supported', () => {
19
+ // Even though 'pt' is requested, 'en' is all that's available.
20
+ expect(() => swapLocale(lhr, 'pt')).toThrow('Unsupported locale \'pt\'');
21
+ });
22
+
23
+ return;
24
+ }
25
+
26
+ it('does not mutate the original lhr', () => {
27
+ /** @type {LH.Result} */
28
+ const lhrClone = JSON.parse(JSON.stringify(lhr));
29
+
30
+ const lhrPt = swapLocale(lhr, 'pt').lhr;
31
+ expect(lhrPt).not.toStrictEqual(lhr);
32
+ expect(lhr).toStrictEqual(lhrClone);
33
+ });
34
+
35
+ it('can change golden LHR english strings into german', () => {
36
+ /** @type {LH.Result} */
37
+ const lhrEn = JSON.parse(JSON.stringify(lhr));
38
+ const lhrDe = swapLocale(lhrEn, 'de').lhr;
39
+
40
+ // Basic replacement
41
+ expect(lhrEn.audits.plugins.title).toEqual('Document avoids plugins');
42
+ expect(lhrDe.audits.plugins.title).toEqual('Dokument verwendet keine Plug-ins');
43
+
44
+ // With ICU string argument values
45
+ expect(lhrEn.audits['dom-size'].displayValue).toMatchInlineSnapshot(`"153 elements"`);
46
+ /* eslint-disable no-irregular-whitespace */
47
+ expect(lhrDe.audits['dom-size'].displayValue).toMatchInlineSnapshot(`"153 Elemente"`);
48
+
49
+ // Renderer formatted strings
50
+ expect(lhrEn.i18n.rendererFormattedStrings.labDataTitle).toEqual('Lab Data');
51
+ expect(lhrDe.i18n.rendererFormattedStrings.labDataTitle).toEqual('Labdaten');
52
+
53
+ // Formatted numbers in placeholders.
54
+ expect(lhrEn.audits['mainthread-work-breakdown'].displayValue).
55
+ toMatchInlineSnapshot(`"2.2 s"`);
56
+ expect(lhrDe.audits['mainthread-work-breakdown'].displayValue).
57
+ toMatchInlineSnapshot(`"2,2 s"`);
58
+ /* eslint-enable no-irregular-whitespace */
59
+ });
60
+
61
+ it('can roundtrip back to english correctly', () => {
62
+ /** @type {LH.Result} */
63
+ const lhrEn = JSON.parse(JSON.stringify(lhr));
64
+
65
+ // via Spanish
66
+ const lhrEnEsRT = swapLocale(swapLocale(lhrEn, 'es').lhr, 'en-US').lhr;
67
+ expect(lhrEnEsRT).toEqual(lhrEn);
68
+
69
+ // via Arabic
70
+ const lhrEnArRT = swapLocale(swapLocale(lhrEn, 'ar').lhr, 'en-US').lhr;
71
+ expect(lhrEnArRT).toEqual(lhrEn);
72
+ });
73
+
74
+ it('leaves alone messages where there is no translation available', () => {
75
+ const miniLHR = {
76
+ audits: {
77
+ redirects: {
78
+ id: 'redirects',
79
+ title: 'Avoid multiple page redirects',
80
+ doesntExist: 'A string that does not have localized versions',
81
+ },
82
+ fakeaudit: {
83
+ id: 'fakeaudit',
84
+ title: 'An audit without translations',
85
+ },
86
+ },
87
+ configSettings: {
88
+ locale: 'en-US',
89
+ },
90
+ i18n: {
91
+ icuMessagePaths: {
92
+ 'lighthouse-core/audits/redirects.js | title': ['audits.redirects.title'],
93
+ // File that exists, but `doesntExist` message within it does not.
94
+ 'lighthouse-core/audits/redirects.js | doesntExist': ['audits.redirects.doesntExist'],
95
+ // File and message which do not exist.
96
+ 'lighthouse-core/audits/fakeaudit.js | title': ['audits.fakeaudit.title'],
97
+ },
98
+ },
99
+ };
100
+ const {missingIcuMessageIds} = swapLocale(miniLHR, 'es');
101
+
102
+ // Updated strings are not found, so these remain in the original language
103
+ expect(missingIcuMessageIds).toMatchInlineSnapshot(`
104
+ Array [
105
+ "lighthouse-core/audits/redirects.js | doesntExist",
106
+ "lighthouse-core/audits/fakeaudit.js | title",
107
+ ]
108
+ `);
109
+ });
110
+
111
+ it('does not change properties that are not strings', () => {
112
+ // Unlikely, but possible e.g. if an audit details changed shape over LH versions.
113
+ const miniLhr = {
114
+ audits: {
115
+ redirects: {
116
+ id: 'redirects',
117
+ title: 'Avoid multiple page redirects',
118
+ },
119
+ },
120
+ configSettings: {
121
+ locale: 'en-US',
122
+ },
123
+ i18n: {
124
+ icuMessagePaths: {
125
+ // Points to audit object, not string.
126
+ 'lighthouse-core/audits/redirects.js | title': ['audits.redirects'],
127
+ // Path does not point to anything in LHR.
128
+ 'lighthouse-core/audits/redirects.js | description': ['gatherers..X'],
129
+ },
130
+ },
131
+ };
132
+ const testLocale = 'ru';
133
+ const {lhr} = swapLocale(miniLhr, testLocale);
134
+
135
+ // LHR remains unchanged except for locale and injected `rendererFormattedStrings`.
136
+ miniLhr.configSettings.locale = testLocale;
137
+ miniLhr.i18n.rendererFormattedStrings = expect.any(Object);
138
+ expect(lhr).toEqual(miniLhr);
139
+ });
140
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../.tmp/tsbuildinfo/shared/",
5
+
6
+ // Limit defs to base JS.
7
+ "lib": ["es2020"],
8
+ // Only include `@types/node` from node_modules/.
9
+ "types": ["node"],
10
+ // "listFiles": true,
11
+ },
12
+ "references": [
13
+ {"path": "../types/lhr/"},
14
+ ],
15
+ "include": [
16
+ "**/*.js",
17
+ "types/**/*.d.ts",
18
+ ],
19
+ "exclude": [
20
+ "test/**/*.js",
21
+ ],
22
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+
7
+ import {IcuMessage as IcuMessage_} from '../../types/lhr/i18n';
8
+ import LHResult from '../../types/lhr/lhr';
9
+ import FlowResult_ from '../../types/lhr/flow';
10
+
11
+ import {Locale as Locale_} from '../../types/lhr/settings';
12
+
13
+ declare global {
14
+ // Expose global types in LH namespace.
15
+ module LH {
16
+ export import Result = LHResult;
17
+ export import FlowResult = FlowResult_;
18
+ export type IcuMessage = IcuMessage_;
19
+ export type IcuMessagePaths = LHResult.IcuMessagePaths;
20
+ export type Locale = Locale_;
21
+ }
22
+ }
23
+
24
+ export {};