diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 309404db1e..db642936a6 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -279,6 +279,10 @@ Array [ overflow: visible; } +.c4:focus-visible { + outline: none; +} + .c4 .highlight { position: relative; z-index: 1; diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 46c53e6993..a0db60e079 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -97,6 +97,7 @@ export default class PageComponent extends Component { // Wait for the mathjax promise set by postProcess from previous or current componentDidUpdate call. await Promise.all(this.processing); + this.container.current?.focus(); this.scrollToTopOrHashManager(prevProps.scrollToTopOrHash, this.props.scrollToTopOrHash); const searchHighlightsChanged = !isEqual( diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 4d1179453e..b39f87b69d 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -26,6 +26,10 @@ export default styled(MainContent)` ${contentTextStyle} overflow: visible; + :focus-visible { + outline: none; + } + @media screen { flex: 1; display: flex; diff --git a/src/app/content/components/Page/scrollToTopOrHashManager.ts b/src/app/content/components/Page/scrollToTopOrHashManager.ts index b44a85bca1..66c23732d6 100644 --- a/src/app/content/components/Page/scrollToTopOrHashManager.ts +++ b/src/app/content/components/Page/scrollToTopOrHashManager.ts @@ -2,7 +2,7 @@ import { HTMLElement } from '@openstax/types/lib.dom'; import { scrollTo } from '../../../domUtils'; import * as selectNavigation from '../../../navigation/selectors'; import { AppState } from '../../../types'; -import { assertWindow, memoizeStateToProps, resetTabIndex } from '../../../utils'; +import { assertWindow, memoizeStateToProps } from '../../../utils'; import { isSearchScrollTarget } from '../../search/guards'; import { selectedResult } from '../../search/selectors'; import * as select from '../../selectors'; @@ -26,20 +26,14 @@ const scrollToTarget = (container: HTMLElement | null, hash: string) => { } }; -const scrollToTargetOrTop = (container: HTMLElement | null, hash: string) => { +const scrollToTargetOrTop = (container: HTMLElement | null, hash: string, previous: boolean) => { if (getScrollTarget(container, hash)) { scrollToTarget(container, hash); - } else { - scrollToTop(); + } else if (previous) { + assertWindow().scrollTo(0, 0); } }; -const scrollToTop = () => { - const window = assertWindow(); - resetTabIndex(window.document); - window.scrollTo(0, 0); -}; - const getScrollTarget = (container: HTMLElement | null, hash: string): HTMLElement | null => { return container && typeof(window) !== 'undefined' && hash ? container.querySelector(`[id="${hash.replace(/^#/, '')}"]`) @@ -59,7 +53,7 @@ const scrollToTopOrHashManager = ( return; } if (previous?.page !== current.page) { - scrollToTargetOrTop(container, current.hash); + scrollToTargetOrTop(container, current.hash, Boolean(previous)); } else if (previous?.hash !== current.hash) { scrollToTarget(container, current.hash); } diff --git a/src/app/content/components/Topbar/__snapshots__/index.spec.tsx.snap b/src/app/content/components/Topbar/__snapshots__/index.spec.tsx.snap index 16ba148ea0..3a52769aee 100644 --- a/src/app/content/components/Topbar/__snapshots__/index.spec.tsx.snap +++ b/src/app/content/components/Topbar/__snapshots__/index.spec.tsx.snap @@ -690,7 +690,6 @@ exports[`text resizer does not render if textSize is null 1`] = ` > { }; }); - const render = (options?: TestRendererOptions) => renderer.create( - - - - - - , options); + const render = (options?: TestRendererOptions) => + renderer.create( + + + + + + + , + options + ); -const dispatchSearchShortcut = (target: HTMLElement | undefined) => { - dispatchKeyDownEvent({code: searchKeyCombination.code, altKey: searchKeyCombination.altKey, target}); -}; + const dispatchSearchShortcut = (target: HTMLElement | undefined) => { + dispatchKeyDownEvent({ + code: searchKeyCombination.code, + altKey: searchKeyCombination.altKey, + target, + }); + }; it('opens and closes mobile interface', () => { const component = render(); @@ -87,11 +101,10 @@ const dispatchSearchShortcut = (target: HTMLElement | undefined) => { }); expect(mobileSearch.props.mobileToolbarOpen).toBe(false); expect(event.preventDefault).toHaveBeenCalledTimes(2); - }); - it('goes between main and search input when no search results', () => { - const {node} = renderToDom( + it('goes to main when no search results', () => { + const { node } = renderToDom( @@ -103,13 +116,12 @@ const dispatchSearchShortcut = (target: HTMLElement | undefined) => { ); const tb = node.querySelector('[class*="TopBar"]'); - expect(document?.activeElement?.tagName).toBe('INPUT'); act(() => dispatchSearchShortcut(tb!)); expect(document?.activeElement?.tagName).toBe('MAIN'); }); it('goes to search results when provided', () => { - const {node} = renderToDom( + const { node } = renderToDom( @@ -127,12 +139,14 @@ const dispatchSearchShortcut = (target: HTMLElement | undefined) => { act(() => dispatchSearchShortcut(tb!)); expect(document?.activeElement?.tagName).toBe('INPUT'); act(() => dispatchSearchShortcut(tb!)); - expect(document?.activeElement?.classList.contains('SearchResultsBar')).toBe(true); + expect( + document?.activeElement?.classList.contains('SearchResultsBar') + ).toBe(true); }); it('aborts on mobile', () => { (useMatchMobileQuery as jest.Mock).mockReturnValue(true); - const {node} = renderToDom( + const { node } = renderToDom( @@ -190,14 +204,20 @@ const dispatchSearchShortcut = (target: HTMLElement | undefined) => { }); const htmlelement = document.createElement('div'); - Object.defineProperty(document, 'activeElement', {value: htmlelement, writable: true}); + Object.defineProperty(document, 'activeElement', { + value: htmlelement, + writable: true, + }); const blur1 = jest.spyOn(htmlelement, 'blur'); renderer.act(() => findById('desktop-search').props.onSubmit(makeEvent())); expect(blur1).toHaveBeenCalled(); // test non HTMLElement branch - const svgelement = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - Object.defineProperty(document, 'activeElement', {value: svgelement}); + const svgelement = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ); + Object.defineProperty(document, 'activeElement', { value: svgelement }); const blur2 = jest.spyOn(svgelement, 'blur'); renderer.act(() => findById('desktop-search').props.onSubmit(makeEvent())); expect(blur2).not.toHaveBeenCalled(); @@ -340,7 +360,10 @@ const dispatchSearchShortcut = (target: HTMLElement | undefined) => { jest.spyOn(searchSelectors, 'searchInSidebar').mockReturnValue(true); const component = render(); - expect(component.root.findByProps({ 'data-testid': 'desktop-search' }).props.searchInSidebar).toBe(true); + expect( + component.root.findByProps({ 'data-testid': 'desktop-search' }).props + .searchInSidebar + ).toBe(true); }); }); @@ -355,37 +378,60 @@ describe('search button', () => { jest.restoreAllMocks(); }); - const render = () => renderer.create(); + const render = () => + renderer.create( + + + + ); it('button has theme bg color applied', () => { - const color = searchSelectors.searchButtonColor.resultFunc('bannerColorButton', book, 'blue'); + const color = searchSelectors.searchButtonColor.resultFunc( + 'bannerColorButton', + book, + 'blue' + ); jest.spyOn(searchSelectors, 'searchButtonColor').mockReturnValue(color); jest.spyOn(searchSelectors, 'mobileToolbarOpen').mockReturnValue(true); const component = render(); - const [searchButton, searchButtonMobile] = component.root.findAllByType(SearchButton); + const [searchButton, searchButtonMobile] = component.root.findAllByType( + SearchButton + ); expect(searchButton.props.colorSchema).toEqual('blue'); expect(searchButtonMobile.props.colorSchema).toEqual('blue'); }); it('button has gray bg color applied', () => { - const color = searchSelectors.searchButtonColor.resultFunc('grayButton', book, 'red'); + const color = searchSelectors.searchButtonColor.resultFunc( + 'grayButton', + book, + 'red' + ); jest.spyOn(searchSelectors, 'searchButtonColor').mockReturnValue(color); const component = render(); - const [searchButton, searchButtonMobile] = component.root.findAllByType(SearchButton); + const [searchButton, searchButtonMobile] = component.root.findAllByType( + SearchButton + ); expect(searchButton.props.colorSchema).toEqual('gray'); expect(searchButtonMobile.props.colorSchema).toEqual('gray'); }); it('button has no bg color applied', () => { - const color = searchSelectors.searchButtonColor.resultFunc(null, book, 'blue'); + const color = searchSelectors.searchButtonColor.resultFunc( + null, + book, + 'blue' + ); jest.spyOn(searchSelectors, 'searchButtonColor').mockReturnValue(color); const component = render(); - const [searchButton, searchButtonMobile] = component.root.findAllByType(SearchButton); + const [searchButton, searchButtonMobile] = component.root.findAllByType( + SearchButton + ); expect(searchButton.props.colorSchema).toEqual(null); expect(searchButtonMobile.props.colorSchema).toEqual(null); @@ -398,8 +444,8 @@ describe('search button', () => { const findById = makeFindByTestId(component.root); const inputEvent = makeInputEvent('cool search'); - renderer.act( - () => findById('desktop-search-input').props.onChange(inputEvent) + renderer.act(() => + findById('desktop-search-input').props.onChange(inputEvent) ); const event = makeEvent(); @@ -421,10 +467,16 @@ describe('mobile menu button', () => { }); it('opens mobile menu', () => { - const component = renderer.create(); + const component = renderer.create( + + + + ); renderer.act(() => { - component.root.findByType(MenuButton).props.onClick({preventDefault: jest.fn()}); + component.root + .findByType(MenuButton) + .props.onClick({ preventDefault: jest.fn() }); }); expect(dispatch).toHaveBeenCalledWith(openMobileMenu()); @@ -443,18 +495,28 @@ describe('text resizer', () => { it('does not render if textSize is null', () => { store.dispatch((setTextSize as any)(null)); - const component = renderer.create(); + const component = renderer.create( + + + + ); expect(component.root.findAllByType(TextResizerMenu)).toEqual([]); expect(component).toMatchSnapshot(); }); it('opens menu when clicking menu button', () => { - const component = renderer.create(); + const component = renderer.create( + + + + ); expect(component.root.findAllByType(TextResizerMenu)).toEqual([]); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'text-resizer' }) - .findByProps({ isOpen: false }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'text-resizer' }) + .findByProps({ isOpen: false }) + .props.onClick({ preventDefault: jest.fn() }); }); expect(component.root.findByType(TextResizerMenu)).toBeDefined(); @@ -462,27 +524,39 @@ describe('text resizer', () => { }); it('changes the text size with buttons', () => { - const component = renderer.create(); + const component = renderer.create( + + + + ); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'text-resizer' }) - .findByProps({ isOpen: false }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'text-resizer' }) + .findByProps({ isOpen: false }) + .props.onClick({ preventDefault: jest.fn() }); }); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'decrease-text-size' }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'decrease-text-size' }) + .props.onClick({ preventDefault: jest.fn() }); }); expect(dispatch).toHaveBeenCalledWith(setTextSize(-1)); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'increase-text-size' }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'increase-text-size' }) + .props.onClick({ preventDefault: jest.fn() }); }); expect(dispatch).toHaveBeenCalledWith(setTextSize(0)); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'increase-text-size' }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'increase-text-size' }) + .props.onClick({ preventDefault: jest.fn() }); }); expect(dispatch).toHaveBeenCalledWith(setTextSize(1)); @@ -490,7 +564,8 @@ describe('text resizer', () => { dispatch.mockReset(); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'change-text-size' }) + component.root + .findByProps({ 'data-testid': 'change-text-size' }) .props.onChange({ currentTarget: { value: '3' } }); }); @@ -500,24 +575,38 @@ describe('text resizer', () => { it('keeps values within bounds', () => { store.dispatch(setTextSize(textResizerMaxValue)); dispatch.mockClear(); - const component = renderer.create(); + const component = renderer.create( + + + + ); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'text-resizer' }) - .findByProps({ isOpen: false }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'text-resizer' }) + .findByProps({ isOpen: false }) + .props.onClick({ preventDefault: jest.fn() }); }); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'increase-text-size' }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'increase-text-size' }) + .props.onClick({ preventDefault: jest.fn() }); }); - expect(dispatch).not.toHaveBeenCalledWith(setTextSize((textResizerMaxValue + 1) as any)); + expect(dispatch).not.toHaveBeenCalledWith( + setTextSize((textResizerMaxValue + 1) as any) + ); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'decrease-text-size' }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'decrease-text-size' }) + .props.onClick({ preventDefault: jest.fn() }); }); - expect(dispatch).toHaveBeenCalledWith(setTextSize((textResizerMaxValue - 1 as any))); + expect(dispatch).toHaveBeenCalledWith( + setTextSize((textResizerMaxValue - 1) as any) + ); renderer.act(() => { store.dispatch(setTextSize(textResizerMinValue)); @@ -525,31 +614,41 @@ describe('text resizer', () => { }); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'decrease-text-size' }).props.onClick({ preventDefault: jest.fn() }); + component.root + .findByProps({ 'data-testid': 'decrease-text-size' }) + .props.onClick({ preventDefault: jest.fn() }); }); - expect(dispatch).not.toHaveBeenCalledWith(setTextSize((textResizerMinValue - 1 as any))); + expect(dispatch).not.toHaveBeenCalledWith( + setTextSize((textResizerMinValue - 1) as any) + ); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'change-text-size' }) + component.root + .findByProps({ 'data-testid': 'change-text-size' }) .props.onChange({ currentTarget: { value: textResizerMaxValue + 1 } }); }); - expect(dispatch).not.toHaveBeenCalledWith(setTextSize(textResizerMaxValue + 1 as any)); + expect(dispatch).not.toHaveBeenCalledWith( + setTextSize((textResizerMaxValue + 1) as any) + ); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'change-text-size' }) + component.root + .findByProps({ 'data-testid': 'change-text-size' }) .props.onChange({ currentTarget: { value: textResizerMinValue - 1 } }); }); - expect(dispatch).not.toHaveBeenCalledWith(setTextSize(textResizerMinValue - 1 as any)); + expect(dispatch).not.toHaveBeenCalledWith( + setTextSize((textResizerMinValue - 1) as any) + ); renderer.act(() => { - component.root.findByProps({ 'data-testid': 'change-text-size' }) + component.root + .findByProps({ 'data-testid': 'change-text-size' }) .props.onChange({ currentTarget: { value: 'invalid' } }); }); expect(dispatch).not.toHaveBeenCalled(); }); - }); diff --git a/src/app/content/components/Topbar/index.tsx b/src/app/content/components/Topbar/index.tsx index 72c4f8605d..8b38589473 100644 --- a/src/app/content/components/Topbar/index.tsx +++ b/src/app/content/components/Topbar/index.tsx @@ -167,9 +167,14 @@ function MobileSearchInputWrapper({ colorSchema={searchButtonColor} searchInSidebar={searchInSidebar} > - + {!state.formSubmitted && !newButtonEnabled && } diff --git a/src/app/content/components/Topbar/styled.tsx b/src/app/content/components/Topbar/styled.tsx index 4e0b56ef0a..c972e31498 100644 --- a/src/app/content/components/Topbar/styled.tsx +++ b/src/app/content/components/Topbar/styled.tsx @@ -10,6 +10,7 @@ import { decoratedLinkStyle, textRegularStyle, textStyle } from '../../../compon import theme from '../../../theme'; import { textResizerMaxValue, textResizerMinValue } from '../../constants'; import { BookWithOSWebData } from '../../types'; +import { HTMLInputElement } from '@openstax/types/lib.dom'; import { bookBannerDesktopMiniHeight, bookBannerMobileMiniHeight, @@ -226,29 +227,44 @@ export const SearchInputWrapper = styled.form` `; // tslint:disable-next-line:variable-name -export const SearchInput = styled(({ desktop, mobile, ...props }) => - )` - ${textStyle} - ${hideSearchChrome} - font-size: 1.6rem; - margin: 0 1rem 0 1rem; - height: ${toolbarSearchInputHeight}rem; - border: none; - outline: none; - width: 100%; - appearance: textfield; +export const SearchInput = styled(({ desktop, mobile, autoFocus, ...props }) => { + const ref = React.useRef(null); - ::placeholder { - color: ${theme.color.text.label}; + React.useEffect( + () => { + if (autoFocus) { + ref.current?.focus(); } + }, + [autoFocus] + ); - ${(props) => props.desktop && theme.breakpoints.mobileMedium(css` - display: none; - `)} - `; + return ( + + ); +})` + ${textStyle} + ${hideSearchChrome} + font-size: 1.6rem; + margin: 0 1rem 0 1rem; + height: ${toolbarSearchInputHeight}rem; + border: none; + outline: none; + width: 100%; + appearance: textfield; + + ::placeholder { + color: ${theme.color.text.label}; + } + + ${(props) => props.desktop && theme.breakpoints.mobileMedium(css` + display: none; + `)} +`; // tslint:disable-next-line:variable-name export const SearchPrintWrapper = isVerticalNavOpenConnector(styled.div` diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index a7ad86049e..1e2cb75386 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -712,6 +712,10 @@ Array [ overflow: visible; } +.c79:focus-visible { + outline: none; +} + .c79 .highlight { position: relative; z-index: 1; @@ -3299,7 +3303,6 @@ li[aria-label="Current Page"] .c61 { > { }; }; -export const resetTabIndex = (document: Document) => { - const index = document.body.tabIndex; - document.body.tabIndex = 0; - - document.body.focus(); - document.body.tabIndex = index; -}; - export const preventDefault = (event: React.MouseEvent) => { event.preventDefault(); return event; diff --git a/src/test/lighthouse.ts b/src/test/lighthouse.ts index 1e26b7c76b..9f126ddd41 100644 --- a/src/test/lighthouse.ts +++ b/src/test/lighthouse.ts @@ -17,11 +17,19 @@ export const checkLighthouse = async(target: Browser, urlPath: string, scoreTarg const result: ScoreTargets = {}; testedCategories.forEach((category) => { - const { score } = lhr.categories[category]; + const categoryReport = lhr.categories[category]; + const { score, auditRefs } = categoryReport; if (scoreTargets) { const minScore = scoreTargets[category]; if (minScore && score < minScore) { + + auditRefs.forEach(auditRef => { + const audit = lhr.audits[auditRef.id]; + if (auditRef.weight > 0 && audit.score < 1) { + console.log(JSON.stringify(audit, null, 2)); // tslint:disable-line:no-console + } + }); throw new Error(`${category} score of ${score} was less than the minimum of ${minScore}`); } } diff --git a/src/typings/lighthouse.d.ts b/src/typings/lighthouse.d.ts index b5f55e39ec..873e95a7e2 100644 --- a/src/typings/lighthouse.d.ts +++ b/src/typings/lighthouse.d.ts @@ -2,127 +2,6 @@ declare module 'lighthouse' { type NotYetTyped = any; // Anything that has not been defined yet. - type AuditId = - 'is-on-https' - | 'redirects-http' - | 'service-worker' - | 'works-offline' - | 'viewport' - | 'without-javascript' - | 'first-contentful-paint' - | 'first-meaningful-paint' - | 'load-fast-enough-for-pwa' - | 'speed-index' - | 'screenshot-thumbnails' - | 'final-screenshot' - | 'estimated-input-latency' - | 'errors-in-console' - | 'time-to-first-byte' - | 'first-cpu-idle' - | 'interactive' - | 'user-timings' - | 'critical-request-chains' - | 'redirects' - | 'webapp-install-banner' - | 'splash-screen' - | 'themed-omnibox' - | 'manifest-short-name-length' - | 'content-width' - | 'image-aspect-ratio' - | 'deprecations' - | 'mainthread-work-breakdown' - | 'bootup-time' - | 'uses-rel-preload' - | 'uses-rel-preconnect' - | 'font-display' - | 'network-requests' - | 'metrics' - | 'pwa-cross-browser' - | 'pwa-page-transitions' - | 'pwa-each-page-has-url' - | 'accesskeys' - | 'aria-allowed-attr' - | 'aria-required-attr' - | 'aria-required-children' - | 'aria-required-parent' - | 'aria-roles' - | 'aria-valid-attr-value' - | 'aria-valid-attr' - | 'audio-caption' - | 'button-name' - | 'bypass' - | 'color-contrast' - | 'definition-list' - | 'dlitem' - | 'document-title' - | 'duplicate-id' - | 'frame-title' - | 'html-has-lang' - | 'html-lang-valid' - | 'image-alt' - | 'input-image-alt' - | 'label' - | 'layout-table' - | 'link-name' - | 'list' - | 'listitem' - | 'meta-refresh' - | 'meta-viewport' - | 'object-alt' - | 'tabindex' - | 'td-headers-attr' - | 'th-has-data-cells' - | 'valid-lang' - | 'video-caption' - | 'video-description' - | 'custom-controls-labels' - | 'custom-controls-roles' - | 'focus-traps' - | 'focusable-controls' - | 'heading-levels' - | 'interactive-element-affordance' - | 'logical-tab-order' - | 'managed-focus' - | 'offscreen-content-hidden' - | 'use-landmarks' - | 'visual-order-follows-dom' - | 'uses-long-cache-ttl' - | 'total-byte-weight' - | 'offscreen-images' - | 'render-blocking-resources' - | 'unminified-css' - | 'unminified-javascript' - | 'unused-css-rules' - | 'uses-webp-images' - | 'uses-optimized-images' - | 'uses-text-compression' - | 'uses-responsive-images' - | 'efficient-animated-content' - | 'appcache-manifest' - | 'doctype' - | 'dom-size' - | 'external-anchors-use-rel-noopener' - | 'geolocation-on-start' - | 'no-document-write' - | 'no-vulnerable-libraries' - | 'js-libraries' - | 'no-websql' - | 'notification-on-start' - | 'password-inputs-can-be-pasted-into' - | 'uses-http2' - | 'uses-passive-event-listeners' - | 'meta-description' - | 'http-status-code' - | 'font-size' - | 'link-text' - | 'is-crawlable' - | 'robots-txt' - | 'hreflang' - | 'plugins' - | 'canonical' - | 'mobile-friendly' - | 'structured-data'; - type ScoreDisplayMode = 'binary' | 'numeric' @@ -131,7 +10,7 @@ declare module 'lighthouse' { | 'not-applicable'; interface AuditResult { - id: AuditId; + id: string; title: string; description: string; score: number; @@ -148,7 +27,7 @@ declare module 'lighthouse' { title: string; id: string; score: number; - auditRefs: NotYetTyped[]; + auditRefs: Array<{id: string; weight: number}>; manualDescription?: string; } @@ -169,7 +48,7 @@ declare module 'lighthouse' { message: string }; - audits: {[k: AuditId]: AuditResult}; + audits: {[k: string]: AuditResult}; timing: { total: number };