diff --git a/packages/api-explorer/e2e/diffScene.spec.ts b/packages/api-explorer/e2e/diffScene.spec.ts index d32210a94..3f8d0e839 100644 --- a/packages/api-explorer/e2e/diffScene.spec.ts +++ b/packages/api-explorer/e2e/diffScene.spec.ts @@ -151,7 +151,7 @@ describe('Diff Scene', () => { page.evaluate((el) => el.innerText.match(/^[a-z_]*/)[0], resultCard) ) ) - expect(page1Methods).toHaveLength(15) + expect(page1Methods).toHaveLength(16) expect(page1Methods).toContain('delete_board_item') } @@ -176,13 +176,15 @@ describe('Diff Scene', () => { const methodLink = await page.$(`${resultCardsSelector} a[role=link]`) expect(methodLink).not.toBeNull() const methodText = await page.evaluate((e) => e.innerText, methodLink) - expect(methodText).toMatch(`delete_board_item for 4.0`) + expect(methodText).toMatch(`delete_alert for 4.0`) // Click and validate destination await methodLink.click() await page.waitForSelector(`div[class*=MethodBadge]`, { timeout: 5000 }) const compUrl = page.url() - expect(compUrl).toEqual(`${BASE_URL}/4.0/methods/Board/delete_board_item`) + expect(compUrl).toEqual( + `${BASE_URL}/4.0/methods/Alert/delete_alert?sdk=py` + ) } }) @@ -217,7 +219,7 @@ describe('Diff Scene', () => { // Check the URL // Would like to do this earlier, but not sure what to wait on const compUrl = page.url() - expect(compUrl).toEqual(`${BASE_URL}/diff/3.1/4.0`) + expect(compUrl).toEqual(`${BASE_URL}/diff/3.1/4.0?sdk=py`) // Check the results const diffResultCards = await page.$$(resultCardsSelector) @@ -228,7 +230,7 @@ describe('Diff Scene', () => { ) ) - expect(diff31to40Page1Methods).toHaveLength(15) + expect(diff31to40Page1Methods).toHaveLength(16) expect(diff31to40Page1Methods).toContain('delete_board_item') // Click the switch button @@ -245,7 +247,7 @@ describe('Diff Scene', () => { await page.waitForTimeout(150) const switchUrl = page.url() - expect(switchUrl).toEqual(`${BASE_URL}/diff/4.0/3.1`) + expect(switchUrl).toEqual(`${BASE_URL}/diff/4.0/3.1?sdk=py`) // Check the results again, even though they should be the same const diff40to31Page1Methods = await Promise.all( @@ -254,7 +256,7 @@ describe('Diff Scene', () => { ) ) - expect(diff40to31Page1Methods).toHaveLength(15) + expect(diff40to31Page1Methods).toHaveLength(16) expect(diff40to31Page1Methods).toContain('delete_board_item') }) }) diff --git a/packages/api-explorer/e2e/e2e.spec.ts b/packages/api-explorer/e2e/e2e.spec.ts index bfdd2952f..1d01deff7 100644 --- a/packages/api-explorer/e2e/e2e.spec.ts +++ b/packages/api-explorer/e2e/e2e.spec.ts @@ -45,6 +45,12 @@ describe('API Explorer', () => { await goToPage(v40) }) + afterEach(async () => { + await page.evaluate(() => { + localStorage.clear() + }) + }) + it('renders a method page', async () => { await Promise.all([ page.waitForNavigation(), @@ -55,7 +61,7 @@ describe('API Explorer', () => { expect(page).toClick('h3', { text: 'Get All Dashboards' }), ]) await expect(page.url()).toEqual( - `${v40}/methods/Dashboard/all_dashboards` + `${v40}/methods/Dashboard/all_dashboards?sdk=py` ) // title @@ -245,6 +251,12 @@ describe('API Explorer', () => { await goToPage(v40) }) + afterEach(async () => { + await page.evaluate(() => { + localStorage.clear() + }) + }) + it('searches methods', async () => { await expect(page).toFill('input[aria-label="Search"]', 'get workspace') // TODO: find a better way to avoid the scenario where L215 executes before search returns @@ -255,7 +267,9 @@ describe('API Explorer', () => { await expect(page).toMatchElement('button', { text: 'Types (0)' }) await expect(page).toClick('a', { text: 'Get Workspace' }) await expect(page).toMatchElement('h2', { text: 'Get Workspace' }) - await expect(page.url()).toEqual(`${v40}/methods/Workspace/workspace`) + await expect(page.url()).toEqual( + `${v40}/methods/Workspace/workspace?sdk=py&s=get+workspace` + ) }) it('searches types', async () => { @@ -267,7 +281,9 @@ describe('API Explorer', () => { await expect(page).toClick('button', { text: 'Types (1)' }) await expect(page).toClick('a', { text: 'WriteTheme' }) await expect(page).toMatchElement('h2', { text: 'WriteTheme' }) - await expect(page.url()).toEqual(`${v40}/types/Theme/WriteTheme`) + await expect(page.url()).toEqual( + `${v40}/types/Theme/WriteTheme?sdk=py&s=writetheme` + ) }) }) }) diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index 05740eb17..84e835763 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -67,8 +67,9 @@ import { useSpecStoreState, selectSpecs, selectCurrentSpec, + selectSdkLanguage, } from './state' -import { getSpecKey, diffPath } from './utils' +import { getSpecKey, diffPath, useNavigation, findSdk, allAlias } from './utils' export interface ApiExplorerProps { adaptor: IApixAdaptor @@ -85,19 +86,23 @@ export const ApiExplorer: FC = ({ declarationsLodeUrl = `${apixFilesHost}/declarationsIndex.json`, headless = false, }) => { - useSettingStoreState() + const { initialized } = useSettingStoreState() useLodesStoreState() const { working, description } = useSpecStoreState() const specs = useSelector(selectSpecs) const spec = useSelector(selectCurrentSpec) + const selectedSdkLanguage = useSelector(selectSdkLanguage) const { initLodesAction } = useLodeActions() - const { initSettingsAction, setSearchPatternAction } = useSettingActions() + const { initSettingsAction, setSearchPatternAction, setSdkLanguageAction } = + useSettingActions() const { initSpecsAction, setCurrentSpecAction } = useSpecActions() const location = useLocation() + const navigate = useNavigation() const [hasNavigation, setHasNavigation] = useState(true) const toggleNavigation = (target?: boolean) => setHasNavigation(target || !hasNavigation) + const searchParams = new URLSearchParams(location.search) const hasNavigationToggle = useCallback((e: MessageEvent) => { if (e.origin === window.origin && e.data.action === 'toggle_sidebar') { @@ -116,6 +121,29 @@ export const ApiExplorer: FC = ({ return () => unregisterEnvAdaptor() }, []) + useEffect(() => { + // reconcile local storage state with URL or vice versa + if (initialized) { + const sdkParam = searchParams.get('sdk') || '' + const sdk = findSdk(sdkParam) + const validSdkParam = + !sdkParam.localeCompare(sdk.alias, 'en', { sensitivity: 'base' }) || + !sdkParam.localeCompare(sdk.language, 'en', { sensitivity: 'base' }) + if (validSdkParam) { + // sync store with URL + setSdkLanguageAction({ + sdkLanguage: sdk.language, + }) + } else { + // sync URL with store + const { alias } = findSdk(selectedSdkLanguage) + navigate(location.pathname, { + sdk: alias === allAlias ? null : alias, + }) + } + } + }, [initialized]) + useEffect(() => { const maybeSpec = location.pathname?.split('/')[1] if (spec && maybeSpec && maybeSpec !== diffPath && maybeSpec !== spec.key) { @@ -124,9 +152,12 @@ export const ApiExplorer: FC = ({ }, [location.pathname, spec]) useEffect(() => { - const searchParams = new URLSearchParams(location.search) + if (!initialized) return const searchPattern = searchParams.get('s') || '' - setSearchPatternAction({ searchPattern: searchPattern! }) + const sdkParam = searchParams.get('sdk') || 'all' + const { language: sdkLanguage } = findSdk(sdkParam) + setSearchPatternAction({ searchPattern }) + setSdkLanguageAction({ sdkLanguage }) }, [location.search]) useEffect(() => { diff --git a/packages/api-explorer/src/components/DocSDKs/DocSDKs.spec.tsx b/packages/api-explorer/src/components/DocSDKs/DocSDKs.spec.tsx index 7b73d20ac..e31115e11 100644 --- a/packages/api-explorer/src/components/DocSDKs/DocSDKs.spec.tsx +++ b/packages/api-explorer/src/components/DocSDKs/DocSDKs.spec.tsx @@ -66,7 +66,10 @@ describe('DocSDKs', () => { 'it can render a %s method declaration', (sdkLanguage) => { store = createTestStore({ - settings: { initialized: false, sdkLanguage }, + settings: { + initialized: false, + sdkLanguage: sdkLanguage, + }, }) renderWithReduxProvider( , diff --git a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx index 9991e4ce8..d8565fd7d 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx @@ -24,61 +24,88 @@ */ import React from 'react' -import { act, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { codeGenerators } from '@looker/sdk-codegen' -import * as reactRedux from 'react-redux' - -import { registerTestEnvAdaptor } from '@looker/extension-utils' -import { defaultSettingsState, settingsSlice } from '../../state' -import { renderWithReduxProvider } from '../../test-utils' +import { + createTestStore, + renderWithRouterAndReduxProvider, +} from '../../test-utils' +import { findSdk } from '../../utils' +import { languages } from '../../test-data' +import { defaultSettingsState } from '../../state' import { SdkLanguageSelector } from './SdkLanguageSelector' +const mockHistoryPush = jest.fn() +jest.mock('react-router-dom', () => { + const ReactRouterDOM = jest.requireActual('react-router-dom') + return { + ...ReactRouterDOM, + useHistory: () => ({ + push: mockHistoryPush, + location, + }), + } +}) describe('SdkLanguageSelector', () => { window.HTMLElement.prototype.scrollIntoView = jest.fn() + const store = createTestStore({ settings: { initialized: true } }) beforeEach(() => { localStorage.clear() }) - test('it has the correct default language selected', () => { - renderWithReduxProvider() + test('it has the correct default language selected', async () => { + renderWithRouterAndReduxProvider(, undefined, store) expect(screen.getByRole('textbox')).toHaveValue( defaultSettingsState.sdkLanguage ) }) test('it lists all available languages and "All" as options', async () => { - renderWithReduxProvider() - await act(async () => { - await userEvent.click(screen.getByRole('textbox')) - await waitFor(() => { - expect(screen.getAllByRole('option')).toHaveLength( - codeGenerators.length + 1 - ) + renderWithRouterAndReduxProvider(, undefined, store) + userEvent.click(screen.getByRole('textbox')) + await waitFor(() => { + expect(screen.getAllByRole('option')).toHaveLength( + codeGenerators.length + 1 + ) + languages.forEach((language) => { + expect( + screen.getByRole('option', { name: language }) + ).toBeInTheDocument() }) }) }) - test('it stores the selected language in localStorage', async () => { - registerTestEnvAdaptor() - const mockDispatch = jest.fn() - jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch) - renderWithReduxProvider() - - const selector = screen.getByRole('textbox') - expect(defaultSettingsState.sdkLanguage).toEqual('Python') - expect(selector).toHaveValue('Python') - - userEvent.click(selector) - await act(async () => { - await userEvent.click(screen.getByRole('option', { name: 'TypeScript' })) + test.each(languages.filter((l) => l !== 'All'))( + 'choosing `%s` pushes its alias to url', + async (language) => { + renderWithRouterAndReduxProvider( + , + undefined, + store + ) + const selector = screen.getByRole('textbox') + userEvent.click(selector) await waitFor(async () => { - expect(mockDispatch).toHaveBeenLastCalledWith( - settingsSlice.actions.setSdkLanguageAction({ - sdkLanguage: 'TypeScript', - }) - ) + await userEvent.click(screen.getByRole('option', { name: language })) + const sdk = findSdk(language) + expect(mockHistoryPush).toHaveBeenLastCalledWith({ + pathname: location.pathname, + search: `sdk=${sdk.alias}`, + }) + }) + } + ) + + test("choosing 'All' removes sdk parameter from the url", async () => { + renderWithRouterAndReduxProvider(, undefined, store) + userEvent.click(screen.getByRole('textbox')) + await waitFor(async () => { + await userEvent.click(screen.getByRole('option', { name: 'All' })) + expect(mockHistoryPush).toHaveBeenLastCalledWith({ + pathname: location.pathname, + search: '', }) }) }) diff --git a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx index ba42bb350..8e1a98dd4 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx @@ -24,43 +24,36 @@ */ import type { FC } from 'react' -import React from 'react' -import { codeGenerators } from '@looker/sdk-codegen' +import React, { useEffect, useState } from 'react' import { Select } from '@looker/components' import { useSelector } from 'react-redux' -import type { SelectOptionProps } from '@looker/components' - -import { useSettingActions, selectSdkLanguage } from '../../state' +import { selectSdkLanguage } from '../../state' +import { allAlias, useNavigation } from '../../utils' +import { allSdkLanguageOptions } from './utils' /** * Allows the user to select their preferred SDK language */ export const SdkLanguageSelector: FC = () => { - const { setSdkLanguageAction } = useSettingActions() + const navigate = useNavigation() const selectedSdkLanguage = useSelector(selectSdkLanguage) + const [language, setLanguage] = useState(selectedSdkLanguage) + const options = allSdkLanguageOptions() - const allSdkLanguages: SelectOptionProps[] = codeGenerators.map((gen) => ({ - value: gen.language, - })) - - allSdkLanguages.push({ - options: [ - { - value: 'All', - }, - ], - }) - - const handleChange = (language: string) => { - setSdkLanguageAction({ sdkLanguage: language }) + const handleChange = (alias: string) => { + navigate(location.pathname, { sdk: alias === allAlias ? null : alias }) } + useEffect(() => { + setLanguage(selectedSdkLanguage) + }, [selectedSdkLanguage]) + return (