diff --git a/lighthouse-cli/bin.js b/lighthouse-cli/bin.js index b90e4f731ab5..1cafc139ccb0 100644 --- a/lighthouse-cli/bin.js +++ b/lighthouse-cli/bin.js @@ -97,8 +97,8 @@ if (cliFlags.extraHeaders) { */ async function run() { if (cliFlags.printConfig) { - const config = generateConfig(cliFlags, configJson); - process.stdout.write(config.print()); + const config = generateConfig(configJson, cliFlags); + process.stdout.write(config.getPrintString()); return; } diff --git a/lighthouse-cli/cli-flags.js b/lighthouse-cli/cli-flags.js index 8cfd2b1fee29..055ab21bdf3d 100644 --- a/lighthouse-cli/cli-flags.js +++ b/lighthouse-cli/cli-flags.js @@ -99,7 +99,7 @@ function getFlags(manualArgv) { 'only-audits': 'Only run the specified audits', 'only-categories': 'Only run the specified categories', 'skip-audits': 'Run everything except these audits', - 'print-config': 'Print the full config that the current options will run with and exit.', + 'print-config': 'Print the full, normalized config for the given config and options, then exit.', }) // set aliases .alias({'gather-mode': 'G', 'audit-mode': 'A'}) @@ -140,14 +140,13 @@ function getFlags(manualArgv) { .default('enable-error-reporting', undefined) // Undefined so prompted by default .check(/** @param {LH.CliFlags} argv */ (argv) => { // Lighthouse doesn't need a URL if... - // - We're in auditMode (and we have artifacts already) // - We're just listing the available options. // - We're just printing the config. - // If one of these don't apply, stop the program and ask for a url. - const isListMode = argv.listAllAudits || argv.listTraceCategories; + // - We're in auditMode (and we have artifacts already) + // If one of these don't apply, if no URL, stop the program and ask for one. + const isPrintSomethingMode = argv.listAllAudits || argv.listTraceCategories || argv.printConfig; const isOnlyAuditMode = !!argv.auditMode && !argv.gatherMode; - const isPrintConfigMode = argv.printConfig; - if (isListMode || isOnlyAuditMode || isPrintConfigMode) { + if (isPrintSomethingMode || isOnlyAuditMode) { return true; } else if (argv._.length > 0) { return true; diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap new file mode 100644 index 000000000000..80717ab64200 --- /dev/null +++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap @@ -0,0 +1,1316 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CLI Tests print-config should print the default config and exit immediately after 1`] = ` +Object { + "audits": Array [ + Object { + "path": "is-on-https", + }, + Object { + "path": "redirects-http", + }, + Object { + "path": "service-worker", + }, + Object { + "path": "works-offline", + }, + Object { + "path": "viewport", + }, + Object { + "path": "without-javascript", + }, + Object { + "path": "metrics/first-contentful-paint", + }, + Object { + "path": "metrics/first-meaningful-paint", + }, + Object { + "path": "load-fast-enough-for-pwa", + }, + Object { + "path": "metrics/speed-index", + }, + Object { + "path": "screenshot-thumbnails", + }, + Object { + "path": "final-screenshot", + }, + Object { + "path": "metrics/estimated-input-latency", + }, + Object { + "path": "errors-in-console", + }, + Object { + "path": "time-to-first-byte", + }, + Object { + "path": "metrics/first-cpu-idle", + }, + Object { + "path": "metrics/interactive", + }, + Object { + "path": "user-timings", + }, + Object { + "path": "critical-request-chains", + }, + Object { + "path": "redirects", + }, + Object { + "path": "webapp-install-banner", + }, + Object { + "path": "splash-screen", + }, + Object { + "path": "themed-omnibox", + }, + Object { + "path": "manifest-short-name-length", + }, + Object { + "path": "content-width", + }, + Object { + "path": "image-aspect-ratio", + }, + Object { + "path": "deprecations", + }, + Object { + "path": "mainthread-work-breakdown", + }, + Object { + "path": "bootup-time", + }, + Object { + "path": "uses-rel-preload", + }, + Object { + "path": "uses-rel-preconnect", + }, + Object { + "path": "font-display", + }, + Object { + "path": "network-requests", + }, + Object { + "path": "metrics", + }, + Object { + "path": "manual/pwa-cross-browser", + }, + Object { + "path": "manual/pwa-page-transitions", + }, + Object { + "path": "manual/pwa-each-page-has-url", + }, + Object { + "path": "accessibility/accesskeys", + }, + Object { + "path": "accessibility/aria-allowed-attr", + }, + Object { + "path": "accessibility/aria-required-attr", + }, + Object { + "path": "accessibility/aria-required-children", + }, + Object { + "path": "accessibility/aria-required-parent", + }, + Object { + "path": "accessibility/aria-roles", + }, + Object { + "path": "accessibility/aria-valid-attr-value", + }, + Object { + "path": "accessibility/aria-valid-attr", + }, + Object { + "path": "accessibility/audio-caption", + }, + Object { + "path": "accessibility/button-name", + }, + Object { + "path": "accessibility/bypass", + }, + Object { + "path": "accessibility/color-contrast", + }, + Object { + "path": "accessibility/definition-list", + }, + Object { + "path": "accessibility/dlitem", + }, + Object { + "path": "accessibility/document-title", + }, + Object { + "path": "accessibility/duplicate-id", + }, + Object { + "path": "accessibility/frame-title", + }, + Object { + "path": "accessibility/html-has-lang", + }, + Object { + "path": "accessibility/html-lang-valid", + }, + Object { + "path": "accessibility/image-alt", + }, + Object { + "path": "accessibility/input-image-alt", + }, + Object { + "path": "accessibility/label", + }, + Object { + "path": "accessibility/layout-table", + }, + Object { + "path": "accessibility/link-name", + }, + Object { + "path": "accessibility/list", + }, + Object { + "path": "accessibility/listitem", + }, + Object { + "path": "accessibility/meta-refresh", + }, + Object { + "path": "accessibility/meta-viewport", + }, + Object { + "path": "accessibility/object-alt", + }, + Object { + "path": "accessibility/tabindex", + }, + Object { + "path": "accessibility/td-headers-attr", + }, + Object { + "path": "accessibility/th-has-data-cells", + }, + Object { + "path": "accessibility/valid-lang", + }, + Object { + "path": "accessibility/video-caption", + }, + Object { + "path": "accessibility/video-description", + }, + Object { + "path": "accessibility/manual/custom-controls-labels", + }, + Object { + "path": "accessibility/manual/custom-controls-roles", + }, + Object { + "path": "accessibility/manual/focus-traps", + }, + Object { + "path": "accessibility/manual/focusable-controls", + }, + Object { + "path": "accessibility/manual/heading-levels", + }, + Object { + "path": "accessibility/manual/interactive-element-affordance", + }, + Object { + "path": "accessibility/manual/logical-tab-order", + }, + Object { + "path": "accessibility/manual/managed-focus", + }, + Object { + "path": "accessibility/manual/offscreen-content-hidden", + }, + Object { + "path": "accessibility/manual/use-landmarks", + }, + Object { + "path": "accessibility/manual/visual-order-follows-dom", + }, + Object { + "path": "byte-efficiency/uses-long-cache-ttl", + }, + Object { + "path": "byte-efficiency/total-byte-weight", + }, + Object { + "path": "byte-efficiency/offscreen-images", + }, + Object { + "path": "byte-efficiency/render-blocking-resources", + }, + Object { + "path": "byte-efficiency/unminified-css", + }, + Object { + "path": "byte-efficiency/unminified-javascript", + }, + Object { + "path": "byte-efficiency/unused-css-rules", + }, + Object { + "path": "byte-efficiency/uses-webp-images", + }, + Object { + "path": "byte-efficiency/uses-optimized-images", + }, + Object { + "path": "byte-efficiency/uses-text-compression", + }, + Object { + "path": "byte-efficiency/uses-responsive-images", + }, + Object { + "path": "byte-efficiency/efficient-animated-content", + }, + Object { + "path": "dobetterweb/appcache-manifest", + }, + Object { + "path": "dobetterweb/doctype", + }, + Object { + "path": "dobetterweb/dom-size", + }, + Object { + "path": "dobetterweb/external-anchors-use-rel-noopener", + }, + Object { + "path": "dobetterweb/geolocation-on-start", + }, + Object { + "path": "dobetterweb/no-document-write", + }, + Object { + "path": "dobetterweb/no-vulnerable-libraries", + }, + Object { + "path": "dobetterweb/js-libraries", + }, + Object { + "path": "dobetterweb/no-websql", + }, + Object { + "path": "dobetterweb/notification-on-start", + }, + Object { + "path": "dobetterweb/password-inputs-can-be-pasted-into", + }, + Object { + "path": "dobetterweb/uses-http2", + }, + Object { + "path": "dobetterweb/uses-passive-event-listeners", + }, + Object { + "path": "seo/meta-description", + }, + Object { + "path": "seo/http-status-code", + }, + Object { + "path": "seo/font-size", + }, + Object { + "path": "seo/link-text", + }, + Object { + "path": "seo/is-crawlable", + }, + Object { + "path": "seo/robots-txt", + }, + Object { + "path": "seo/hreflang", + }, + Object { + "path": "seo/plugins", + }, + Object { + "path": "seo/canonical", + }, + Object { + "path": "seo/manual/mobile-friendly", + }, + Object { + "path": "seo/manual/structured-data", + }, + ], + "categories": Object { + "accessibility": Object { + "auditRefs": Array [ + Object { + "group": "a11y-correct-attributes", + "id": "accesskeys", + "weight": 1, + }, + Object { + "group": "a11y-aria", + "id": "aria-allowed-attr", + "weight": 3, + }, + Object { + "group": "a11y-aria", + "id": "aria-required-attr", + "weight": 2, + }, + Object { + "group": "a11y-aria", + "id": "aria-required-children", + "weight": 5, + }, + Object { + "group": "a11y-aria", + "id": "aria-required-parent", + "weight": 2, + }, + Object { + "group": "a11y-aria", + "id": "aria-roles", + "weight": 3, + }, + Object { + "group": "a11y-aria", + "id": "aria-valid-attr-value", + "weight": 2, + }, + Object { + "group": "a11y-aria", + "id": "aria-valid-attr", + "weight": 5, + }, + Object { + "group": "a11y-correct-attributes", + "id": "audio-caption", + "weight": 4, + }, + Object { + "group": "a11y-element-names", + "id": "button-name", + "weight": 10, + }, + Object { + "group": "a11y-describe-contents", + "id": "bypass", + "weight": 10, + }, + Object { + "group": "a11y-color-contrast", + "id": "color-contrast", + "weight": 6, + }, + Object { + "group": "a11y-well-structured", + "id": "definition-list", + "weight": 1, + }, + Object { + "group": "a11y-well-structured", + "id": "dlitem", + "weight": 1, + }, + Object { + "group": "a11y-describe-contents", + "id": "document-title", + "weight": 2, + }, + Object { + "group": "a11y-well-structured", + "id": "duplicate-id", + "weight": 5, + }, + Object { + "group": "a11y-describe-contents", + "id": "frame-title", + "weight": 5, + }, + Object { + "group": "a11y-language", + "id": "html-has-lang", + "weight": 4, + }, + Object { + "group": "a11y-language", + "id": "html-lang-valid", + "weight": 1, + }, + Object { + "group": "a11y-correct-attributes", + "id": "image-alt", + "weight": 8, + }, + Object { + "group": "a11y-correct-attributes", + "id": "input-image-alt", + "weight": 1, + }, + Object { + "group": "a11y-describe-contents", + "id": "label", + "weight": 10, + }, + Object { + "group": "a11y-describe-contents", + "id": "layout-table", + "weight": 1, + }, + Object { + "group": "a11y-element-names", + "id": "link-name", + "weight": 9, + }, + Object { + "group": "a11y-well-structured", + "id": "list", + "weight": 5, + }, + Object { + "group": "a11y-well-structured", + "id": "listitem", + "weight": 4, + }, + Object { + "group": "a11y-meta", + "id": "meta-refresh", + "weight": 1, + }, + Object { + "group": "a11y-meta", + "id": "meta-viewport", + "weight": 3, + }, + Object { + "group": "a11y-describe-contents", + "id": "object-alt", + "weight": 4, + }, + Object { + "group": "a11y-correct-attributes", + "id": "tabindex", + "weight": 4, + }, + Object { + "group": "a11y-correct-attributes", + "id": "td-headers-attr", + "weight": 1, + }, + Object { + "group": "a11y-correct-attributes", + "id": "th-has-data-cells", + "weight": 1, + }, + Object { + "group": "a11y-language", + "id": "valid-lang", + "weight": 1, + }, + Object { + "group": "a11y-describe-contents", + "id": "video-caption", + "weight": 4, + }, + Object { + "group": "a11y-describe-contents", + "id": "video-description", + "weight": 3, + }, + Object { + "id": "logical-tab-order", + "weight": 0, + }, + Object { + "id": "focusable-controls", + "weight": 0, + }, + Object { + "id": "interactive-element-affordance", + "weight": 0, + }, + Object { + "id": "managed-focus", + "weight": 0, + }, + Object { + "id": "focus-traps", + "weight": 0, + }, + Object { + "id": "custom-controls-labels", + "weight": 0, + }, + Object { + "id": "custom-controls-roles", + "weight": 0, + }, + Object { + "id": "visual-order-follows-dom", + "weight": 0, + }, + Object { + "id": "offscreen-content-hidden", + "weight": 0, + }, + Object { + "id": "heading-levels", + "weight": 0, + }, + Object { + "id": "use-landmarks", + "weight": 0, + }, + ], + "description": "These checks highlight opportunities to [improve the accessibility of your web app](https://developers.google.com/web/fundamentals/accessibility). Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.", + "manualDescription": "These items address areas which an automated testing tool cannot cover. Learn more in our guide on [conducting an accessibility review](https://developers.google.com/web/fundamentals/accessibility/how-to-review).", + "title": "Accessibility", + }, + "best-practices": Object { + "auditRefs": Array [ + Object { + "id": "appcache-manifest", + "weight": 1, + }, + Object { + "id": "no-websql", + "weight": 1, + }, + Object { + "id": "is-on-https", + "weight": 1, + }, + Object { + "id": "uses-http2", + "weight": 1, + }, + Object { + "id": "uses-passive-event-listeners", + "weight": 1, + }, + Object { + "id": "no-document-write", + "weight": 1, + }, + Object { + "id": "external-anchors-use-rel-noopener", + "weight": 1, + }, + Object { + "id": "geolocation-on-start", + "weight": 1, + }, + Object { + "id": "doctype", + "weight": 1, + }, + Object { + "id": "no-vulnerable-libraries", + "weight": 1, + }, + Object { + "id": "js-libraries", + "weight": 0, + }, + Object { + "id": "notification-on-start", + "weight": 1, + }, + Object { + "id": "deprecations", + "weight": 1, + }, + Object { + "id": "password-inputs-can-be-pasted-into", + "weight": 1, + }, + Object { + "id": "errors-in-console", + "weight": 1, + }, + Object { + "id": "image-aspect-ratio", + "weight": 1, + }, + ], + "title": "Best Practices", + }, + "performance": Object { + "auditRefs": Array [ + Object { + "group": "metrics", + "id": "first-contentful-paint", + "weight": 3, + }, + Object { + "group": "metrics", + "id": "first-meaningful-paint", + "weight": 1, + }, + Object { + "group": "metrics", + "id": "speed-index", + "weight": 4, + }, + Object { + "group": "metrics", + "id": "interactive", + "weight": 5, + }, + Object { + "group": "metrics", + "id": "first-cpu-idle", + "weight": 2, + }, + Object { + "group": "metrics", + "id": "estimated-input-latency", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "render-blocking-resources", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "uses-responsive-images", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "offscreen-images", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "unminified-css", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "unminified-javascript", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "unused-css-rules", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "uses-optimized-images", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "uses-webp-images", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "uses-text-compression", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "uses-rel-preconnect", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "time-to-first-byte", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "redirects", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "uses-rel-preload", + "weight": 0, + }, + Object { + "group": "load-opportunities", + "id": "efficient-animated-content", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "total-byte-weight", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "uses-long-cache-ttl", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "dom-size", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "critical-request-chains", + "weight": 0, + }, + Object { + "id": "network-requests", + "weight": 0, + }, + Object { + "id": "metrics", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "user-timings", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "bootup-time", + "weight": 0, + }, + Object { + "id": "screenshot-thumbnails", + "weight": 0, + }, + Object { + "id": "final-screenshot", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "mainthread-work-breakdown", + "weight": 0, + }, + Object { + "group": "diagnostics", + "id": "font-display", + "weight": 0, + }, + ], + "title": "Performance", + }, + "pwa": Object { + "auditRefs": Array [ + Object { + "id": "load-fast-enough-for-pwa", + "weight": 7, + }, + Object { + "id": "works-offline", + "weight": 5, + }, + Object { + "id": "webapp-install-banner", + "weight": 3, + }, + Object { + "id": "is-on-https", + "weight": 2, + }, + Object { + "id": "redirects-http", + "weight": 2, + }, + Object { + "id": "viewport", + "weight": 2, + }, + Object { + "id": "service-worker", + "weight": 1, + }, + Object { + "id": "without-javascript", + "weight": 1, + }, + Object { + "id": "splash-screen", + "weight": 1, + }, + Object { + "id": "themed-omnibox", + "weight": 1, + }, + Object { + "id": "content-width", + "weight": 1, + }, + Object { + "id": "manifest-short-name-length", + "weight": 0, + }, + Object { + "id": "pwa-cross-browser", + "weight": 0, + }, + Object { + "id": "pwa-page-transitions", + "weight": 0, + }, + Object { + "id": "pwa-each-page-has-url", + "weight": 0, + }, + ], + "description": "These checks validate the aspects of a Progressive Web App, as specified by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist).", + "manualDescription": "These checks are required by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist) but are not automatically checked by Lighthouse. They do not affect your score but it's important that you verify them manually.", + "title": "Progressive Web App", + }, + "seo": Object { + "auditRefs": Array [ + Object { + "group": "seo-mobile", + "id": "viewport", + "weight": 1, + }, + Object { + "group": "seo-content", + "id": "document-title", + "weight": 1, + }, + Object { + "group": "seo-content", + "id": "meta-description", + "weight": 1, + }, + Object { + "group": "seo-crawl", + "id": "http-status-code", + "weight": 1, + }, + Object { + "group": "seo-content", + "id": "link-text", + "weight": 1, + }, + Object { + "group": "seo-crawl", + "id": "is-crawlable", + "weight": 1, + }, + Object { + "group": "seo-crawl", + "id": "robots-txt", + "weight": 1, + }, + Object { + "group": "seo-content", + "id": "hreflang", + "weight": 1, + }, + Object { + "group": "seo-content", + "id": "canonical", + "weight": 1, + }, + Object { + "group": "seo-mobile", + "id": "font-size", + "weight": 1, + }, + Object { + "group": "seo-content", + "id": "plugins", + "weight": 1, + }, + Object { + "id": "mobile-friendly", + "weight": 0, + }, + Object { + "id": "structured-data", + "weight": 0, + }, + ], + "description": "These checks ensure that your page is optimized for search engine results ranking. There are additional factors Lighthouse does not check that may affect your search ranking. [Learn more](https://support.google.com/webmasters/answer/35769).", + "manualDescription": "Run these additional validators on your site to check additional SEO best practices.", + "title": "SEO", + }, + }, + "groups": Object { + "a11y-aria": Object { + "description": "These are opportunities to improve the usage of ARIA in your application which may enhance the experience for users of assistive technology, like a screen reader.", + "title": "ARIA Attributes Follow Best Practices", + }, + "a11y-color-contrast": Object { + "description": "These are opportunities to improve the legibility of your content.", + "title": "Color Contrast Is Satisfactory", + }, + "a11y-correct-attributes": Object { + "description": "These are opportunities to improve the configuration of your HTML elements.", + "title": "Elements Use Attributes Correctly", + }, + "a11y-describe-contents": Object { + "description": "These are opportunities to make your content easier to understand for a user of assistive technology, like a screen reader.", + "title": "Elements Describe Contents Well", + }, + "a11y-element-names": Object { + "description": "These are opportunities to improve the semantics of the controls in your application. This may enhance the experience for users of assistive technology, like a screen reader.", + "title": "Elements Have Discernible Names", + }, + "a11y-language": Object { + "description": "These are opportunities to improve the interpretation of your content by users in different locales.", + "title": "Page Specifies Valid Language", + }, + "a11y-meta": Object { + "description": "These are opportunities to improve the user experience of your site.", + "title": "Meta Tags Used Properly", + }, + "a11y-well-structured": Object { + "description": "These are opportunities to make sure your HTML is appropriately structured.", + "title": "Elements Are Well Structured", + }, + "diagnostics": Object { + "description": "More information about the performance of your application.", + "title": "Diagnostics", + }, + "load-opportunities": Object { + "description": "These optimizations can speed up your page load.", + "title": "Opportunities", + }, + "metrics": Object { + "title": "Metrics", + }, + "seo-content": Object { + "description": "Format your HTML in a way that enables crawlers to better understand your app’s content.", + "title": "Content Best Practices", + }, + "seo-crawl": Object { + "description": "To appear in search results, crawlers need access to your app.", + "title": "Crawling and Indexing", + }, + "seo-mobile": Object { + "description": "Make sure your pages are mobile friendly so users don’t have to pinch or zoom in order to read the content pages. [Learn more](https://developers.google.com/search/mobile-sites/).", + "title": "Mobile Friendly", + }, + }, + "passes": Array [ + Object { + "blankDuration": 300, + "blankPage": "about:blank", + "blockedUrlPatterns": Array [], + "cpuQuietThresholdMs": 1000, + "gatherers": Array [ + Object { + "path": "scripts", + }, + Object { + "path": "css-usage", + }, + Object { + "path": "viewport", + }, + Object { + "path": "viewport-dimensions", + }, + Object { + "path": "theme-color", + }, + Object { + "path": "manifest", + }, + Object { + "path": "runtime-exceptions", + }, + Object { + "path": "chrome-console-messages", + }, + Object { + "path": "image-usage", + }, + Object { + "path": "accessibility", + }, + Object { + "path": "dobetterweb/anchors-with-no-rel-noopener", + }, + Object { + "path": "dobetterweb/appcache", + }, + Object { + "path": "dobetterweb/doctype", + }, + Object { + "path": "dobetterweb/domstats", + }, + Object { + "path": "dobetterweb/js-libraries", + }, + Object { + "path": "dobetterweb/optimized-images", + }, + Object { + "path": "dobetterweb/password-inputs-with-prevented-paste", + }, + Object { + "path": "dobetterweb/response-compression", + }, + Object { + "path": "dobetterweb/tags-blocking-first-paint", + }, + Object { + "path": "dobetterweb/websql", + }, + Object { + "path": "seo/meta-description", + }, + Object { + "path": "seo/font-size", + }, + Object { + "path": "seo/crawlable-links", + }, + Object { + "path": "seo/meta-robots", + }, + Object { + "path": "seo/hreflang", + }, + Object { + "path": "seo/embedded-content", + }, + Object { + "path": "seo/canonical", + }, + Object { + "path": "seo/robots-txt", + }, + Object { + "path": "fonts", + }, + ], + "networkQuietThresholdMs": 1000, + "passName": "defaultPass", + "pauseAfterLoadMs": 1000, + "recordTrace": true, + "useThrottling": true, + }, + Object { + "blankDuration": 300, + "blankPage": "about:blank", + "blockedUrlPatterns": Array [], + "cpuQuietThresholdMs": 0, + "gatherers": Array [ + Object { + "path": "service-worker", + }, + Object { + "path": "offline", + }, + Object { + "path": "start-url", + }, + ], + "networkQuietThresholdMs": 0, + "passName": "offlinePass", + "pauseAfterLoadMs": 0, + "recordTrace": false, + "useThrottling": false, + }, + Object { + "blankDuration": 300, + "blankPage": "about:blank", + "blockedUrlPatterns": Array [ + "*.css", + "*.jpg", + "*.jpeg", + "*.png", + "*.gif", + "*.svg", + "*.ttf", + "*.woff", + "*.woff2", + ], + "cpuQuietThresholdMs": 0, + "gatherers": Array [ + Object { + "path": "http-redirect", + }, + Object { + "path": "html-without-javascript", + }, + ], + "networkQuietThresholdMs": 0, + "passName": "redirectPass", + "pauseAfterLoadMs": 0, + "recordTrace": false, + "useThrottling": false, + }, + ], + "settings": Object { + "additionalTraceCategories": null, + "auditMode": false, + "blockedUrlPatterns": null, + "disableDeviceEmulation": false, + "disableStorageReset": false, + "extraHeaders": null, + "gatherMode": false, + "locale": "en-US", + "maxWaitForLoad": 45000, + "onlyAudits": null, + "onlyCategories": null, + "output": Array [ + "html", + ], + "skipAudits": null, + "throttling": Object { + "cpuSlowdownMultiplier": 4, + "downloadThroughputKbps": 1474.5600000000002, + "requestLatencyMs": 562.5, + "rttMs": 150, + "throughputKbps": 1638.4, + "uploadThroughputKbps": 675, + }, + "throttlingMethod": "simulate", + }, +} +`; + +exports[`CLI Tests print-config should print the overridden config and exit immediately after 1`] = ` +Object { + "audits": Array [ + Object { + "path": "metrics", + }, + ], + "categories": Object { + "performance": Object { + "auditRefs": Array [ + Object { + "id": "metrics", + "weight": 0, + }, + ], + "title": "Performance", + }, + }, + "groups": Object { + "a11y-aria": Object { + "description": "These are opportunities to improve the usage of ARIA in your application which may enhance the experience for users of assistive technology, like a screen reader.", + "title": "ARIA Attributes Follow Best Practices", + }, + "a11y-color-contrast": Object { + "description": "These are opportunities to improve the legibility of your content.", + "title": "Color Contrast Is Satisfactory", + }, + "a11y-correct-attributes": Object { + "description": "These are opportunities to improve the configuration of your HTML elements.", + "title": "Elements Use Attributes Correctly", + }, + "a11y-describe-contents": Object { + "description": "These are opportunities to make your content easier to understand for a user of assistive technology, like a screen reader.", + "title": "Elements Describe Contents Well", + }, + "a11y-element-names": Object { + "description": "These are opportunities to improve the semantics of the controls in your application. This may enhance the experience for users of assistive technology, like a screen reader.", + "title": "Elements Have Discernible Names", + }, + "a11y-language": Object { + "description": "These are opportunities to improve the interpretation of your content by users in different locales.", + "title": "Page Specifies Valid Language", + }, + "a11y-meta": Object { + "description": "These are opportunities to improve the user experience of your site.", + "title": "Meta Tags Used Properly", + }, + "a11y-well-structured": Object { + "description": "These are opportunities to make sure your HTML is appropriately structured.", + "title": "Elements Are Well Structured", + }, + "diagnostics": Object { + "description": "More information about the performance of your application.", + "title": "Diagnostics", + }, + "load-opportunities": Object { + "description": "These optimizations can speed up your page load.", + "title": "Opportunities", + }, + "metrics": Object { + "title": "Metrics", + }, + "seo-content": Object { + "description": "Format your HTML in a way that enables crawlers to better understand your app’s content.", + "title": "Content Best Practices", + }, + "seo-crawl": Object { + "description": "To appear in search results, crawlers need access to your app.", + "title": "Crawling and Indexing", + }, + "seo-mobile": Object { + "description": "Make sure your pages are mobile friendly so users don’t have to pinch or zoom in order to read the content pages. [Learn more](https://developers.google.com/search/mobile-sites/).", + "title": "Mobile Friendly", + }, + }, + "passes": Array [ + Object { + "blankDuration": 300, + "blankPage": "about:blank", + "blockedUrlPatterns": Array [], + "cpuQuietThresholdMs": 1000, + "gatherers": Array [], + "networkQuietThresholdMs": 1000, + "passName": "defaultPass", + "pauseAfterLoadMs": 1000, + "recordTrace": true, + "useThrottling": true, + }, + ], + "settings": Object { + "additionalTraceCategories": null, + "auditMode": true, + "blockedUrlPatterns": null, + "disableDeviceEmulation": false, + "disableStorageReset": false, + "extraHeaders": null, + "gatherMode": false, + "locale": "en-US", + "maxWaitForLoad": 45000, + "onlyAudits": Array [ + "metrics", + ], + "onlyCategories": null, + "output": Array [ + "json", + ], + "skipAudits": null, + "throttling": Object { + "cpuSlowdownMultiplier": 4, + "downloadThroughputKbps": 1474.5600000000002, + "requestLatencyMs": 562.5, + "rttMs": 150, + "throughputKbps": 1638.4, + "uploadThroughputKbps": 675, + }, + "throttlingMethod": "simulate", + }, +} +`; diff --git a/lighthouse-cli/test/cli/index-test.js b/lighthouse-cli/test/cli/index-test.js index e55360afa2c0..11d31388b313 100644 --- a/lighthouse-cli/test/cli/index-test.js +++ b/lighthouse-cli/test/cli/index-test.js @@ -75,9 +75,8 @@ describe('CLI Tests', function() { const config = JSON.parse(ret.stdout); assert.strictEqual(config.settings.output[0], 'html'); assert.strictEqual(config.settings.auditMode, false); - assert.ok(Array.isArray(config.passes)); - assert.ok(Array.isArray(config.audits)); - config.audits.forEach(audit => assert.ok(audit.path)); + + expect(config).toMatchSnapshot(); }); it('should print the overridden config and exit immediately after', () => { @@ -91,9 +90,9 @@ describe('CLI Tests', function() { const config = JSON.parse(ret.stdout); assert.strictEqual(config.settings.output[0], 'json'); assert.strictEqual(config.settings.auditMode, true); - assert.ok(Array.isArray(config.passes)); - assert.ok(Array.isArray(config.audits)); assert.strictEqual(config.audits.length, 1); + + expect(config).toMatchSnapshot(); }); }); }); diff --git a/lighthouse-core/config/config.js b/lighthouse-core/config/config.js index b4885ba41592..aa5847b1bb2b 100644 --- a/lighthouse-core/config/config.js +++ b/lighthouse-core/config/config.js @@ -371,7 +371,7 @@ class Config { * Audit `implementation` and `instance` do not survive this process. * @return {string} */ - print() { + getPrintString() { const jsonConfig = deepClone(this); if (jsonConfig.passes) { diff --git a/lighthouse-core/index.js b/lighthouse-core/index.js index ee1ed51e5e21..effe0545aff7 100644 --- a/lighthouse-core/index.js +++ b/lighthouse-core/index.js @@ -48,7 +48,7 @@ async function lighthouse(url, flags = {}, configJSON, connection) { flags.logLevel = flags.logLevel || 'error'; log.setLevel(flags.logLevel); - const config = generateConfig(flags, configJSON); + const config = generateConfig(configJSON, flags); connection = connection || new ChromeProtocol(flags.port, flags.hostname); @@ -58,13 +58,13 @@ async function lighthouse(url, flags = {}, configJSON, connection) { /** * Generate a Lighthouse Config. - * @param {LH.Flags=} flags Optional settings for the Lighthouse run. If present, - * they will override any settings in the config. * @param {LH.Config.Json=} configJson Configuration for the Lighthouse run. If * not present, the default config is used. + * @param {LH.Flags=} flags Optional settings for the Lighthouse run. If present, + * they will override any settings in the config. * @return {Config} */ -function generateConfig(flags, configJson) { +function generateConfig(configJson, flags) { return new Config(configJson, flags); } diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js index 6648a3ae8d05..89c198a38ab5 100644 --- a/lighthouse-core/lib/i18n/i18n.js +++ b/lighthouse-core/lib/i18n/i18n.js @@ -231,14 +231,23 @@ function createMessageInstanceIdFn(filename, fileStrings) { return getMessageInstanceIdFn; } +/** + * Returns true if string is an ICUMessage reference. + * @param {string} icuMessageIdOrRawString + * @return {boolean} + */ +function isIcuMessage(icuMessageIdOrRawString) { + return MESSAGE_INSTANCE_ID_QUICK_REGEX.test(icuMessageIdOrRawString) && + MESSAGE_INSTANCE_ID_REGEX.test(icuMessageIdOrRawString); +} + /** * @param {string} icuMessageIdOrRawString * @param {LH.Locale} locale * @return {string} */ function getFormatted(icuMessageIdOrRawString, locale) { - if (MESSAGE_INSTANCE_ID_QUICK_REGEX.test(icuMessageIdOrRawString) && - MESSAGE_INSTANCE_ID_REGEX.test(icuMessageIdOrRawString)) { + if (isIcuMessage(icuMessageIdOrRawString)) { return _resolveIcuMessageInstanceId(icuMessageIdOrRawString, locale).formattedString; } @@ -268,25 +277,24 @@ function _resolveIcuMessageInstanceId(icuMessageInstanceId, locale) { * Recursively walk the input object, looking for property values that are * string references and replace them with their localized values. Primarily * used with the full LHR as input. - * @param {*} lhr + * @param {*} inputObject * @param {LH.Locale} locale * @return {LH.I18NMessages} */ -function replaceIcuMessageInstanceIds(lhr, locale) { +function replaceIcuMessageInstanceIds(inputObject, locale) { /** - * @param {*} objectInLHR + * @param {*} subObject * @param {LH.I18NMessages} icuMessagePaths * @param {string[]} pathInLHR */ - function replaceInObject(objectInLHR, icuMessagePaths, pathInLHR = []) { - if (typeof objectInLHR !== 'object' || !objectInLHR) return; + function replaceInObject(subObject, icuMessagePaths, pathInLHR = []) { + if (typeof subObject !== 'object' || !subObject) return; - for (const [property, value] of Object.entries(objectInLHR)) { + for (const [property, value] of Object.entries(subObject)) { const currentPathInLHR = pathInLHR.concat([property]); // Check to see if the value in the LHR looks like a string reference. If it is, replace it. - if (typeof value === 'string' && MESSAGE_INSTANCE_ID_QUICK_REGEX.test(value) && - MESSAGE_INSTANCE_ID_REGEX.test(value)) { + if (typeof value === 'string' && isIcuMessage(value)) { const {icuMessageInstance, formattedString} = _resolveIcuMessageInstanceId(value, locale); const messageInstancesInLHR = icuMessagePaths[icuMessageInstance.icuMessageId] || []; const currentPathAsString = _formatPathAsString(currentPathInLHR); @@ -297,7 +305,7 @@ function replaceIcuMessageInstanceIds(lhr, locale) { currentPathAsString ); - objectInLHR[property] = formattedString; + subObject[property] = formattedString; icuMessagePaths[icuMessageInstance.icuMessageId] = messageInstancesInLHR; } else { replaceInObject(value, icuMessagePaths, currentPathInLHR); @@ -307,7 +315,7 @@ function replaceIcuMessageInstanceIds(lhr, locale) { /** @type {LH.I18NMessages} */ const icuMessagePaths = {}; - replaceInObject(lhr, icuMessagePaths); + replaceInObject(inputObject, icuMessagePaths); return icuMessagePaths; } @@ -319,5 +327,5 @@ module.exports = { createMessageInstanceIdFn, getFormatted, replaceIcuMessageInstanceIds, - MESSAGE_INSTANCE_ID_REGEX, + isIcuMessage, }; diff --git a/lighthouse-core/test/config/config-test.js b/lighthouse-core/test/config/config-test.js index cf82e252cddc..48c08cd1eb32 100644 --- a/lighthouse-core/test/config/config-test.js +++ b/lighthouse-core/test/config/config-test.js @@ -975,7 +975,7 @@ describe('Config', () => { }); }); - describe('#print', () => { + describe('#getDisplayString', () => { it('doesn\'t include empty gatherer/audit options in output', () => { const gOpt = 'gathererOption'; const aOpt = 'auditOption'; @@ -994,7 +994,7 @@ describe('Config', () => { ], }; - const printed = new Config(configJson).print(); + const printed = new Config(configJson).getPrintString(); const printedConfig = JSON.parse(printed); // Check that options weren't completely eliminated. @@ -1019,13 +1019,13 @@ describe('Config', () => { }); it('prints localized category titles', () => { - const printed = new Config(defaultConfig).print(); + const printed = new Config(defaultConfig).getPrintString(); const printedConfig = JSON.parse(printed); let localizableCount = 0; Object.entries(printedConfig.categories).forEach(([printedCategoryId, printedCategory]) => { const origTitle = origConfig.categories[printedCategoryId].title; - if (i18n.MESSAGE_INSTANCE_ID_REGEX.test(origTitle)) localizableCount++; + if (i18n.isIcuMessage(origTitle)) localizableCount++; const i18nOrigTitle = i18n.getFormatted(origTitle, origConfig.settings.locale); assert.strictEqual(printedCategory.title, i18nOrigTitle); @@ -1038,10 +1038,10 @@ describe('Config', () => { it('prints a valid ConfigJson that can make an identical Config', () => { // depends on defaultConfig having a `path` for all gatherers and audits. const firstConfig = new Config(defaultConfig); - const firstPrint = firstConfig.print(); + const firstPrint = firstConfig.getPrintString(); const secondConfig = new Config(JSON.parse(firstPrint)); - const secondPrint = secondConfig.print(); + const secondPrint = secondConfig.getPrintString(); assert.strictEqual(firstPrint, secondPrint); }); diff --git a/readme.md b/readme.md index 6e759cfc67be..378c660961a0 100644 --- a/readme.md +++ b/readme.md @@ -49,7 +49,7 @@ Configuration: --save-assets Save the trace & devtools log to disk [boolean] --list-all-audits Prints a list of all available audits and exits [boolean] --list-trace-categories Prints a list of all required trace categories and exits [boolean] - --print-config Print the full config that the current options will run with and exit. [boolean] + --print-config Print the full, normalized config for the given config and options, then exit. [boolean] --additional-trace-categories Additional categories to capture with the trace (comma-delimited). --config-path The path to the config JSON. --chrome-flags Custom flags to pass to Chrome (space-delimited). For a full list of flags, see