diff --git a/core/test/config/config-helpers-test.js b/core/test/config/config-helpers-test.js index be92027d20eb..b9fc86f71237 100644 --- a/core/test/config/config-helpers-test.js +++ b/core/test/config/config-helpers-test.js @@ -274,6 +274,28 @@ describe('.resolveSettings', () => { expect(settings).not.toHaveProperty('nonsense'); }); + describe('sets UA string', () => { + it('to default value if provided value is undefined', () => { + const settings = resolveSettings({}, {emulatedUserAgent: undefined}); + expect(settings.emulatedUserAgent).toMatch(/^Mozilla\/5.*moto.*Chrome/); + }); + + it('to default value if provided value is true', () => { + const settings = resolveSettings({}, {emulatedUserAgent: true}); + expect(settings.emulatedUserAgent).toMatch(/^Mozilla\/5.*moto.*Chrome/); + }); + + it('to false if provided value is false', () => { + const settings = resolveSettings({}, {emulatedUserAgent: false}); + expect(settings.emulatedUserAgent).toEqual(false); + }); + + it('to the provided string value if present', () => { + const settings = resolveSettings({}, {emulatedUserAgent: 'Random UA'}); + expect(settings.emulatedUserAgent).toEqual('Random UA'); + }); + }); + describe('budgets', () => { it('initializes budgets', () => { const settings = resolveSettings({ @@ -341,7 +363,7 @@ describe('.resolveSettings', () => { describe('.resolveGathererToDefn', () => { const coreList = Runner.getGathererList(); - it('should expand gatherer path short-hand', async () => { + it('should expand core gatherer', async () => { const result = await resolveGathererToDefn('image-elements', coreList); expect(result).toEqual({ path: 'image-elements', @@ -350,6 +372,15 @@ describe('.resolveGathererToDefn', () => { }); }); + it('should expand gatherer path short-hand', async () => { + const result = await resolveGathererToDefn({path: 'image-elements'}, coreList); + expect(result).toEqual({ + path: 'image-elements', + implementation: ImageElementsGatherer, + instance: expect.any(ImageElementsGatherer), + }); + }); + it('should find relative to configDir', async () => { const configDir = path.resolve(moduleDir, '../../gather/'); const result = await resolveGathererToDefn('gatherers/image-elements', [], configDir); @@ -360,6 +391,19 @@ describe('.resolveGathererToDefn', () => { }); }); + it('should find custom gatherers', async () => { + const result1 = + await resolveGathererToDefn('../fixtures/valid-custom-gatherer', [], moduleDir); + const result2 = + await resolveGathererToDefn('../fixtures/valid-custom-gatherer.js', [], moduleDir); + const result3 = + await resolveGathererToDefn('../fixtures/valid-custom-gatherer.cjs', [], moduleDir); + + expect(result1).toMatchObject({path: '../fixtures/valid-custom-gatherer'}); + expect(result2).toMatchObject({path: '../fixtures/valid-custom-gatherer.js'}); + expect(result3).toMatchObject({path: '../fixtures/valid-custom-gatherer.cjs'}); + }); + it('should expand gatherer impl short-hand', async () => { const result = await resolveGathererToDefn({implementation: ImageElementsGatherer}, coreList); expect(result).toEqual({ @@ -368,18 +412,60 @@ describe('.resolveGathererToDefn', () => { }); }); + it('should expand gatherer instance short-hand', async () => { + const result = await resolveGathererToDefn({instance: new ImageElementsGatherer()}, coreList); + expect(result).toEqual({ + instance: expect.any(ImageElementsGatherer), + }); + }); + + it('should expand gatherer instance directly', async () => { + const result = await resolveGathererToDefn(new ImageElementsGatherer(), coreList); + expect(result).toEqual({ + instance: expect.any(ImageElementsGatherer), + }); + }); + it('throws for invalid gathererDefn', async () => { await expect(resolveGathererToDefn({})).rejects.toThrow(/Invalid Gatherer type/); }); + + it('throws for invalid path type', async () => { + await expect(resolveGathererToDefn({path: 1234})).rejects.toThrow(/Invalid Gatherer type/); + }); + + it('throws but not for missing gatherer when it has a node dependency error', async () => { + const resultPromise = + resolveGathererToDefn('../fixtures/invalid-gatherers/require-error.js', [], moduleDir); + await expect(resultPromise).rejects.toThrow(/Cannot find module/); + }); }); describe('.resolveAuditsToDefns', () => { - it('should expand audit short-hand', async () => { + it('should expand core audit', async () => { const result = await resolveAuditsToDefns(['user-timings']); expect(result).toEqual([{path: 'user-timings', options: {}, implementation: UserTimingsAudit}]); }); + it('should expand audit path short-hand', async () => { + const result = await resolveAuditsToDefns([{path: 'user-timings'}]); + + expect(result).toEqual([{path: 'user-timings', options: {}, implementation: UserTimingsAudit}]); + }); + + it('should expand audit impl short-hand', async () => { + const result = await resolveAuditsToDefns([{implementation: UserTimingsAudit}]); + + expect(result).toEqual([{options: {}, implementation: UserTimingsAudit}]); + }); + + it('should expand audit impl directly', async () => { + const result = await resolveAuditsToDefns([UserTimingsAudit]); + + expect(result).toEqual([{options: {}, implementation: UserTimingsAudit}]); + }); + it('should find relative to configDir', async () => { const configDir = path.resolve(moduleDir, '../../'); const result = await resolveAuditsToDefns(['audits/user-timings'], configDir); @@ -389,6 +475,19 @@ describe('.resolveAuditsToDefns', () => { ]); }); + it('should find custom audits', async () => { + const result = await resolveAuditsToDefns([ + '../fixtures/valid-custom-audit', + '../fixtures/valid-custom-audit.js', + '../fixtures/valid-custom-audit.cjs', + ], moduleDir); + expect(result).toMatchObject([ + {path: '../fixtures/valid-custom-audit', options: {}}, + {path: '../fixtures/valid-custom-audit.js', options: {}}, + {path: '../fixtures/valid-custom-audit.cjs', options: {}}, + ]); + }); + it('should handle multiple audit definition styles', async () => { const result = await resolveAuditsToDefns(['user-timings', {implementation: UserTimingsAudit}]); @@ -411,6 +510,13 @@ describe('.resolveAuditsToDefns', () => { it('throws for invalid auditDefns', async () => { await expect(resolveAuditsToDefns([new Gatherer()])).rejects.toThrow(/Invalid Audit type/); }); + + it('throws but not for missing audit when it has a node dependency error', async () => { + const resultPromise = resolveAuditsToDefns([ + '../fixtures/invalid-audits/require-error.js', + ], moduleDir); + await expect(resultPromise).rejects.toThrow(/Cannot find module/); + }); }); describe('.resolveModulePath', () => { @@ -426,6 +532,12 @@ describe('.resolveModulePath', () => { expect(pathToPlugin).toEqual(require.resolve(pluginName)); }); + it('throws for unknown resource', async () => { + expect(() => { + resolveModulePath('unknown', null, 'audit'); + }).toThrow(/Unable to locate audit: `unknown`/); + }); + describe('plugin paths to a file', () => { it('relative to the current working directory', () => { const pluginName = 'lighthouse-plugin-config-helper'; diff --git a/core/test/config/config-test.js b/core/test/config/config-test.js index 9305783b14c5..eb4fb2c0cbc3 100644 --- a/core/test/config/config-test.js +++ b/core/test/config/config-test.js @@ -140,6 +140,22 @@ describe('Fraggle Rock Config', () => { }); }); + it('is idempotent when using the resolved config as the config input', async () => { + const config = { + extends: 'lighthouse:default', + settings: { + onlyCategories: ['seo'], + }, + }; + + const {resolvedConfig} = await initializeConfig('navigation', config); + expect(Object.keys(resolvedConfig.categories || {})).toEqual(['seo']); + expect(resolvedConfig.settings.onlyCategories).toEqual(['seo']); + + const {resolvedConfig: resolvedConfig2} = await initializeConfig('navigation', resolvedConfig); + expect(resolvedConfig2).toEqual(resolvedConfig); + }); + describe('resolveArtifactDependencies', () => { /** @type {LH.Gatherer.FRGathererInstance} */ let dependencyGatherer; @@ -428,6 +444,12 @@ describe('Fraggle Rock Config', () => { expect(resolvedConfig.categories.performance.auditRefs).toContain('extra-audit'); } }); + + it('should only accept "lighthouse:default" as the extension method', async () => { + extensionConfig.extends = 'something:else'; + const resolvedConfigPromise = initializeConfig('navigation', extensionConfig); + await expect(resolvedConfigPromise).rejects.toThrow(/`lighthouse:default` is the only valid/); + }); }); it('should use failure mode fatal for the fake navigation', async () => { diff --git a/core/test/config/filters-test.js b/core/test/config/filters-test.js index d46ea3f05133..ebfa1ff0f797 100644 --- a/core/test/config/filters-test.js +++ b/core/test/config/filters-test.js @@ -25,6 +25,14 @@ describe('Fraggle Rock Config Filtering', () => { ...auditMeta, }; } + class OptionalAudit extends BaseAudit { + static meta = { + id: 'optional', + requiredArtifacts: /** @type {any} */ (['Snapshot']), + __internalOptionalArtifacts: /** @type {any} */ (['Timespan']), + ...auditMeta, + }; + } class ManualAudit extends BaseAudit { static meta = { id: 'manual', @@ -230,6 +238,16 @@ describe('Fraggle Rock Config Filtering', () => { ]); }); + it('should keep audits only missing optional artifacts', () => { + const partialArtifacts = [{id: 'Snapshot', gatherer: {instance: snapshotGatherer}}]; + audits.push({implementation: OptionalAudit, options: {}}); + expect(filters.filterAuditsByAvailableArtifacts(audits, partialArtifacts)).toEqual([ + {implementation: SnapshotAudit, options: {}}, + {implementation: ManualAudit, options: {}}, + {implementation: OptionalAudit, options: {}}, + ]); + }); + it('should not filter audits with dependencies on base artifacts', () => { class SnapshotWithBase extends BaseAudit { static meta = { @@ -510,6 +528,27 @@ describe('Fraggle Rock Config Filtering', () => { }); }); + it('should combine category and audit filters additively', () => { + const filtered = filters.filterConfigByExplicitFilters(resolvedConfig, { + onlyCategories: ['navigation'], + onlyAudits: ['snapshot', 'timespan'], + skipAudits: [], + }); + expect(filtered).toMatchObject({ + artifacts: [{id: 'Snapshot'}, {id: 'Timespan'}], + audits: [ + {implementation: SnapshotAudit}, + {implementation: TimespanAudit}, + {implementation: NavigationAudit}, + ], + categories: { + navigation: { + auditRefs: [{id: 'navigation'}], + }, + }, + }); + }); + it('should filter out audits and artifacts not in the categories by default', () => { resolvedConfig = { ...resolvedConfig, diff --git a/core/test/config/validation-test.js b/core/test/config/validation-test.js index 04dc8413bd42..051c4e07d0dd 100644 --- a/core/test/config/validation-test.js +++ b/core/test/config/validation-test.js @@ -145,6 +145,14 @@ describe('Fraggle Rock Config Validation', () => { expect(invocation).toThrow(/has no audit.*method/); }); + it('should throw if audit id is missing', () => { + // @ts-expect-error - We are intentionally creating a malformed input. + ExampleAudit.meta.id = undefined; + const audit = {implementation: ExampleAudit, options: {}}; + const invocation = () => validation.assertValidAudit(audit); + expect(invocation).toThrow(/has no meta.id/); + }); + it('should throw if title is missing', () => { // @ts-expect-error - We are intentionally creating a malformed input. ExampleAudit.meta.title = undefined; @@ -153,6 +161,14 @@ describe('Fraggle Rock Config Validation', () => { expect(invocation).toThrow(/has no meta.title/); }); + it('should throw if audit description is missing', () => { + // @ts-expect-error - We are intentionally creating a malformed input. + ExampleAudit.meta.description = undefined; + const audit = {implementation: ExampleAudit, options: {}}; + const invocation = () => validation.assertValidAudit(audit); + expect(invocation).toThrow(/has no meta.description/); + }); + it('should throw if failureTitle is missing', () => { ExampleAudit.meta.failureTitle = undefined; ExampleAudit.meta.scoreDisplayMode = BaseAudit.SCORING_MODES.BINARY;