From f2a1f6ba0222948d1ae606bc7fc14a99d4c6b4dd Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Wed, 18 Dec 2024 14:08:49 +0000 Subject: [PATCH 01/10] FTN-58: initial version without tests --- .../app/search/results/page.tsx | 4 +- .../app/search/results/search-result-data.ts | 36 ---- .../molecules/Search/result/index.tsx | 4 +- .../components/pages/search/results/index.tsx | 2 +- .../lib/search/searchResultData.ts | 56 +++++ .../lib/search/searchService.ts | 62 ++++++ .../fingertips-frontend/package-lock.json | 192 ++++++++++++++++++ frontend/fingertips-frontend/package.json | 1 + 8 files changed, 316 insertions(+), 41 deletions(-) delete mode 100644 frontend/fingertips-frontend/app/search/results/search-result-data.ts create mode 100644 frontend/fingertips-frontend/lib/search/searchResultData.ts create mode 100644 frontend/fingertips-frontend/lib/search/searchService.ts diff --git a/frontend/fingertips-frontend/app/search/results/page.tsx b/frontend/fingertips-frontend/app/search/results/page.tsx index a99f142e..424af4fa 100644 --- a/frontend/fingertips-frontend/app/search/results/page.tsx +++ b/frontend/fingertips-frontend/app/search/results/page.tsx @@ -1,5 +1,5 @@ import { SearchResults } from '@/components/pages/search/results'; -import { getSearchData } from './search-result-data'; +import { getSearchService } from '@/lib/search/searchResultData'; export default async function Page( props: Readonly<{ @@ -12,7 +12,7 @@ export default async function Page( const indicator = searchParams?.indicator ?? ''; // Perform async API call using indicator prop - const searchResults = getSearchData(); + const searchResults = await getSearchService().searchByIndicator(indicator); return ; } diff --git a/frontend/fingertips-frontend/app/search/results/search-result-data.ts b/frontend/fingertips-frontend/app/search/results/search-result-data.ts deleted file mode 100644 index 0af95681..00000000 --- a/frontend/fingertips-frontend/app/search/results/search-result-data.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface IndicatorSearchResult { - id: number; - indicatorName: string; - latestDataPeriod: string; - dataSource: string; - lastUpdated: string; -} - -export const MOCK_DATA: IndicatorSearchResult[] = [ - { - id: 1, - indicatorName: 'NHS', - latestDataPeriod: '2023', - dataSource: 'NHS website', - lastUpdated: formatDate(new Date('December 6, 2024')), - }, - { - id: 2, - indicatorName: 'DHSC', - latestDataPeriod: '2022', - dataSource: 'Student article', - lastUpdated: formatDate(new Date('November 5, 2023')), - }, -]; - -function formatDate(date: Date): string { - const day = String(date.getDate()).padStart(2, '0'); - const month = date.toLocaleString('en-GB', { month: 'long' }); - const year = date.getFullYear(); - - return `${day} ${month} ${year}`; -} - -export const getSearchData = (): IndicatorSearchResult[] => { - return MOCK_DATA; -}; diff --git a/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx b/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx index 52b8bf8e..0fb79938 100644 --- a/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx +++ b/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx @@ -8,7 +8,7 @@ import { } from 'govuk-react'; import { spacing, typography } from '@govuk-react/lib'; -import { IndicatorSearchResult } from '@/app/search/results/search-result-data'; +import { IndicatorSearchResult } from '@/lib/search/searchResultData'; import styled from 'styled-components'; type SearchResultProps = { @@ -16,7 +16,7 @@ type SearchResultProps = { }; const StyledParagraph = styled(Paragraph)( - typography.font({ size: 19, lineHeight: '0.5' }) + typography.font({ size: 19, lineHeight: '1.2' }) ); const StyledRow = styled(GridRow)( diff --git a/frontend/fingertips-frontend/components/pages/search/results/index.tsx b/frontend/fingertips-frontend/components/pages/search/results/index.tsx index 67e6cf32..dfc22393 100644 --- a/frontend/fingertips-frontend/components/pages/search/results/index.tsx +++ b/frontend/fingertips-frontend/components/pages/search/results/index.tsx @@ -10,7 +10,7 @@ import { } from 'govuk-react'; import { SearchResult } from '@/components/molecules/Search/result'; -import { IndicatorSearchResult } from '@/app/search/results/search-result-data'; +import { IndicatorSearchResult } from '@/lib/search/searchResultData'; type SearchResultsProps = { indicator: string; diff --git a/frontend/fingertips-frontend/lib/search/searchResultData.ts b/frontend/fingertips-frontend/lib/search/searchResultData.ts new file mode 100644 index 00000000..5f2a2ad0 --- /dev/null +++ b/frontend/fingertips-frontend/lib/search/searchResultData.ts @@ -0,0 +1,56 @@ +import { SearchService } from './searchService'; + +export interface IndicatorSearchResult { + id: string; + indicatorName: string; + latestDataPeriod?: string; + dataSource?: string; + lastUpdated?: string; +} + +export interface IndicatorSearch { + searchByIndicator(indicator: string): Promise; +} + +export const MOCK_DATA: IndicatorSearchResult[] = [ + { + id: '1', + indicatorName: 'NHS', + latestDataPeriod: '2023', + dataSource: 'NHS website', + lastUpdated: formatDate(new Date('December 6, 2024')), + }, + { + id: '2', + indicatorName: 'DHSC', + latestDataPeriod: '2022', + dataSource: 'Student article', + lastUpdated: formatDate(new Date('November 5, 2023')), + }, +]; + +function formatDate(date: Date): string { + const day = String(date.getDate()).padStart(2, '0'); + const month = date.toLocaleString('en-GB', { month: 'long' }); + const year = date.getFullYear(); + + return `${day} ${month} ${year}`; +} + +let searchService: SearchService; +try { + searchService = new SearchService(); +} catch { + // Handle error +} + +export const getSearchService = (): IndicatorSearch => { + return searchService + ? searchService + : { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + searchByIndicator(indicator: string): Promise { + return Promise.resolve(MOCK_DATA); + }, + }; +}; diff --git a/frontend/fingertips-frontend/lib/search/searchService.ts b/frontend/fingertips-frontend/lib/search/searchService.ts new file mode 100644 index 00000000..9766bac9 --- /dev/null +++ b/frontend/fingertips-frontend/lib/search/searchService.ts @@ -0,0 +1,62 @@ +import { IndicatorSearch, IndicatorSearchResult } from './searchResultData'; +import { AzureKeyCredential, SearchClient } from '@azure/search-documents'; + +type Indicator = { + IID: string; + Descriptive: { + Name: string; + Definition: string; + DataSource: string; + }; + DataChange: { + LastUploadedAt: string; + }; +}; + +const getEnvironmentVariable = (name: string): string => { + return ( + process.env[name] ?? + (() => { + throw new Error(`Missing environment variable ${name}`); + })() + ); +}; + +export class SearchService implements IndicatorSearch { + readonly searchClient: SearchClient; + + constructor() { + const serviceName = getEnvironmentVariable('DHSC_AI_SEARCH_SERVICE_NAME'); + const indexName = getEnvironmentVariable('DHSC_AI_SEARCH_INDEX_NAME'); + const apiKey = getEnvironmentVariable('DHSC_AI_SEARCH_API_KEY'); + + this.searchClient = new SearchClient( + `https://${serviceName}.search.windows.net`, + indexName, + new AzureKeyCredential(apiKey) + ); + } + + async searchByIndicator(indicator: string): Promise { + const query = `/.*${indicator}.*/`; + + const searchResponse = await this.searchClient.search(query, { + queryType: 'full', + searchFields: ['IID'], + includeTotalCount: true, + }); + + const results: IndicatorSearchResult[] = []; + for await (const result of searchResponse.results) { + results.push({ + id: result?.document?.IID, + indicatorName: result?.document?.Descriptive?.Name, + latestDataPeriod: undefined, + dataSource: result.document?.Descriptive?.DataSource, + lastUpdated: result.document?.DataChange?.LastUploadedAt, + }); + } + + return results; + } +} diff --git a/frontend/fingertips-frontend/package-lock.json b/frontend/fingertips-frontend/package-lock.json index b8ccb6d9..95586480 100644 --- a/frontend/fingertips-frontend/package-lock.json +++ b/frontend/fingertips-frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "fingertips-frontend", "version": "0.1.0", "dependencies": { + "@azure/search-documents": "^12.1.0", "@emotion/is-prop-valid": "^1.3.1", "@types/styled-components": "^5.1.34", "govuk-react": "^0.10.7", @@ -83,6 +84,188 @@ "playwright-core": ">= 1.0.0" } }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.1.tgz", + "integrity": "sha512-/wS73UEDrxroUEVywEm7J0p2c+IIiVxyfigCGfsKvCxxCET4V/Hef2aURqltrXMRjNmdmt5IuOgIpl8f6xdO5A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", + "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/search-documents": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@azure/search-documents/-/search-documents-12.1.0.tgz", + "integrity": "sha512-IzD+hfqGqFtXymHXm4RzrZW2MsSH2M7RLmZsKaKVi7SUxbeYTUeX+ALk8gVzkM8ykb7EzlDLWCNErKfAa57rYQ==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.3.0", + "@azure/core-http-compat": "^2.0.1", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.3.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -5637,6 +5820,15 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", diff --git a/frontend/fingertips-frontend/package.json b/frontend/fingertips-frontend/package.json index 4f69cabe..8df12b1c 100644 --- a/frontend/fingertips-frontend/package.json +++ b/frontend/fingertips-frontend/package.json @@ -18,6 +18,7 @@ "prettier-ci": "prettier . --check" }, "dependencies": { + "@azure/search-documents": "^12.1.0", "@emotion/is-prop-valid": "^1.3.1", "@types/styled-components": "^5.1.34", "govuk-react": "^0.10.7", From 0fafeb19f22e3464fcb5fa4db35bde431505d22e Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Wed, 18 Dec 2024 16:31:28 +0000 Subject: [PATCH 02/10] FTN-58: fix existing tests --- .../components/molecules/Search/result/searchResult.test.tsx | 2 +- .../components/pages/search/results/Results.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx b/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx index d0aa325e..9245374a 100644 --- a/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx +++ b/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { expect } from '@jest/globals'; import { SearchResult } from '.'; -import { MOCK_DATA } from '@/app/search/results/search-result-data'; +import { MOCK_DATA } from '@/lib/search/searchResultData'; import { registryWrapper } from '@/lib/testutils'; describe('Search Result Suite', () => { diff --git a/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx b/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx index ca9bb80f..0c50c0c5 100644 --- a/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx +++ b/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { expect } from '@jest/globals'; -import { MOCK_DATA } from '@/app/search/results/search-result-data'; +import { MOCK_DATA } from '@/lib/search/searchResultData'; import { SearchResults } from '.'; import { registryWrapper } from '@/lib/testutils'; From 1e6f0ca8d9c6625e773e61e9475b6aaad2a17c61 Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Mon, 23 Dec 2024 13:41:15 +0000 Subject: [PATCH 03/10] FTN-58: wip --- .../app/search/results/page.tsx | 2 +- .../__snapshots__/searchResult.test.tsx.snap | 6 +-- .../molecules/Search/result/index.tsx | 4 +- .../Search/result/searchResult.test.tsx | 4 +- .../pages/search/results/Results.test.tsx | 2 +- .../__snapshots__/Results.test.tsx.snap | 6 +-- .../components/pages/search/results/index.tsx | 4 +- .../lib/search/searchResultData.ts | 46 ++++--------------- .../lib/search/searchService.ts | 45 ++++++++---------- .../lib/search/searchServiceMock.ts | 33 +++++++++++++ frontend/fingertips-frontend/lib/util.test.ts | 17 ++++++- frontend/fingertips-frontend/lib/utils.ts | 9 ++++ 12 files changed, 99 insertions(+), 79 deletions(-) create mode 100644 frontend/fingertips-frontend/lib/search/searchServiceMock.ts diff --git a/frontend/fingertips-frontend/app/search/results/page.tsx b/frontend/fingertips-frontend/app/search/results/page.tsx index 424af4fa..e505ef35 100644 --- a/frontend/fingertips-frontend/app/search/results/page.tsx +++ b/frontend/fingertips-frontend/app/search/results/page.tsx @@ -12,7 +12,7 @@ export default async function Page( const indicator = searchParams?.indicator ?? ''; // Perform async API call using indicator prop - const searchResults = await getSearchService().searchByIndicator(indicator); + const searchResults = await getSearchService().searchWith(indicator); return ; } diff --git a/frontend/fingertips-frontend/components/molecules/Search/result/__snapshots__/searchResult.test.tsx.snap b/frontend/fingertips-frontend/components/molecules/Search/result/__snapshots__/searchResult.test.tsx.snap index 2ea500ce..e199161f 100644 --- a/frontend/fingertips-frontend/components/molecules/Search/result/__snapshots__/searchResult.test.tsx.snap +++ b/frontend/fingertips-frontend/components/molecules/Search/result/__snapshots__/searchResult.test.tsx.snap @@ -91,7 +91,7 @@ exports[`Search Result Suite snapshot test 1`] = ` -moz-osx-font-smoothing: grayscale; font-weight: 400; font-size: 16px; - line-height: 0.5; + line-height: 1.2; } .c2 { @@ -177,14 +177,14 @@ exports[`Search Result Suite snapshot test 1`] = ` @media print { .c6 { font-size: 14px; - line-height: 0.5; + line-height: 1.2; } } @media only screen and (min-width: 641px) { .c6 { font-size: 19px; - line-height: 0.5; + line-height: 1.2; } } diff --git a/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx b/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx index 0fb79938..4b044ecc 100644 --- a/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx +++ b/frontend/fingertips-frontend/components/molecules/Search/result/index.tsx @@ -8,11 +8,11 @@ import { } from 'govuk-react'; import { spacing, typography } from '@govuk-react/lib'; -import { IndicatorSearchResult } from '@/lib/search/searchResultData'; +import { BasicSearchResult } from '@/lib/search/searchResultData'; import styled from 'styled-components'; type SearchResultProps = { - result: IndicatorSearchResult; + result: BasicSearchResult; }; const StyledParagraph = styled(Paragraph)( diff --git a/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx b/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx index 9245374a..7b832350 100644 --- a/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx +++ b/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { expect } from '@jest/globals'; -import { SearchResult } from '.'; -import { MOCK_DATA } from '@/lib/search/searchResultData'; +import { BasicSearchResult } from '.'; +import { MOCK_DATA } from '@/lib/search/searchServiceMock'; import { registryWrapper } from '@/lib/testutils'; describe('Search Result Suite', () => { diff --git a/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx b/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx index 0c50c0c5..8ec750c4 100644 --- a/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx +++ b/frontend/fingertips-frontend/components/pages/search/results/Results.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { expect } from '@jest/globals'; -import { MOCK_DATA } from '@/lib/search/searchResultData'; +import { MOCK_DATA } from '@/lib/search/searchServiceMock'; import { SearchResults } from '.'; import { registryWrapper } from '@/lib/testutils'; diff --git a/frontend/fingertips-frontend/components/pages/search/results/__snapshots__/Results.test.tsx.snap b/frontend/fingertips-frontend/components/pages/search/results/__snapshots__/Results.test.tsx.snap index 872855d1..a844d141 100644 --- a/frontend/fingertips-frontend/components/pages/search/results/__snapshots__/Results.test.tsx.snap +++ b/frontend/fingertips-frontend/components/pages/search/results/__snapshots__/Results.test.tsx.snap @@ -624,7 +624,7 @@ exports[`Search Results Suite snapshot test 1`] = ` -moz-osx-font-smoothing: grayscale; font-weight: 400; font-size: 16px; - line-height: 0.5; + line-height: 1.2; } .c6 { @@ -768,14 +768,14 @@ exports[`Search Results Suite snapshot test 1`] = ` @media print { .c10 { font-size: 14px; - line-height: 0.5; + line-height: 1.2; } } @media only screen and (min-width: 641px) { .c10 { font-size: 19px; - line-height: 0.5; + line-height: 1.2; } } diff --git a/frontend/fingertips-frontend/components/pages/search/results/index.tsx b/frontend/fingertips-frontend/components/pages/search/results/index.tsx index dfc22393..30626d8c 100644 --- a/frontend/fingertips-frontend/components/pages/search/results/index.tsx +++ b/frontend/fingertips-frontend/components/pages/search/results/index.tsx @@ -10,11 +10,11 @@ import { } from 'govuk-react'; import { SearchResult } from '@/components/molecules/Search/result'; -import { IndicatorSearchResult } from '@/lib/search/searchResultData'; +import { BasicSearchResult } from '@/lib/search/searchResultData'; type SearchResultsProps = { indicator: string; - searchResults: IndicatorSearchResult[]; + searchResults: BasicSearchResult[]; }; export function SearchResults({ diff --git a/frontend/fingertips-frontend/lib/search/searchResultData.ts b/frontend/fingertips-frontend/lib/search/searchResultData.ts index 5f2a2ad0..9a173d9c 100644 --- a/frontend/fingertips-frontend/lib/search/searchResultData.ts +++ b/frontend/fingertips-frontend/lib/search/searchResultData.ts @@ -1,6 +1,7 @@ import { SearchService } from './searchService'; +import { SearchServiceMock } from './searchServiceMock'; -export interface IndicatorSearchResult { +export interface BasicSearchResult { id: string; indicatorName: string; latestDataPeriod?: string; @@ -8,49 +9,18 @@ export interface IndicatorSearchResult { lastUpdated?: string; } -export interface IndicatorSearch { - searchByIndicator(indicator: string): Promise; +export interface Search { + searchWith(searchTerm: string): Promise; } -export const MOCK_DATA: IndicatorSearchResult[] = [ - { - id: '1', - indicatorName: 'NHS', - latestDataPeriod: '2023', - dataSource: 'NHS website', - lastUpdated: formatDate(new Date('December 6, 2024')), - }, - { - id: '2', - indicatorName: 'DHSC', - latestDataPeriod: '2022', - dataSource: 'Student article', - lastUpdated: formatDate(new Date('November 5, 2023')), - }, -]; - -function formatDate(date: Date): string { - const day = String(date.getDate()).padStart(2, '0'); - const month = date.toLocaleString('en-GB', { month: 'long' }); - const year = date.getFullYear(); - - return `${day} ${month} ${year}`; -} - -let searchService: SearchService; +let searchService: Search; try { searchService = new SearchService(); } catch { // Handle error + searchService = new SearchServiceMock(); } -export const getSearchService = (): IndicatorSearch => { - return searchService - ? searchService - : { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - searchByIndicator(indicator: string): Promise { - return Promise.resolve(MOCK_DATA); - }, - }; +export const getSearchService = (): Search => { + return searchService; }; diff --git a/frontend/fingertips-frontend/lib/search/searchService.ts b/frontend/fingertips-frontend/lib/search/searchService.ts index 9766bac9..02f913fd 100644 --- a/frontend/fingertips-frontend/lib/search/searchService.ts +++ b/frontend/fingertips-frontend/lib/search/searchService.ts @@ -1,5 +1,6 @@ -import { IndicatorSearch, IndicatorSearchResult } from './searchResultData'; +import { Search, BasicSearchResult } from './searchResultData'; import { AzureKeyCredential, SearchClient } from '@azure/search-documents'; +import { getEnvironmentVariable } from '../utils'; type Indicator = { IID: string; @@ -13,40 +14,32 @@ type Indicator = { }; }; -const getEnvironmentVariable = (name: string): string => { - return ( - process.env[name] ?? - (() => { - throw new Error(`Missing environment variable ${name}`); - })() - ); -}; - -export class SearchService implements IndicatorSearch { - readonly searchClient: SearchClient; +export class SearchService implements Search { + readonly serviceUrl: string; + readonly indexName: string; + readonly apiKey: string; constructor() { - const serviceName = getEnvironmentVariable('DHSC_AI_SEARCH_SERVICE_NAME'); - const indexName = getEnvironmentVariable('DHSC_AI_SEARCH_INDEX_NAME'); - const apiKey = getEnvironmentVariable('DHSC_AI_SEARCH_API_KEY'); - - this.searchClient = new SearchClient( - `https://${serviceName}.search.windows.net`, - indexName, - new AzureKeyCredential(apiKey) - ); + this.serviceUrl = getEnvironmentVariable('DHSC_AI_SEARCH_SERVICE_URL'); + this.indexName = getEnvironmentVariable('DHSC_AI_SEARCH_INDEX_NAME'); + this.apiKey = getEnvironmentVariable('DHSC_AI_SEARCH_API_KEY'); } - async searchByIndicator(indicator: string): Promise { - const query = `/.*${indicator}.*/`; + async searchWith(searchTerm: string): Promise { + const query = `${searchTerm} /.*${searchTerm}.*/`; + + const searchClient = new SearchClient( + this.serviceUrl, + this.indexName, + new AzureKeyCredential(this.apiKey) + ); - const searchResponse = await this.searchClient.search(query, { + const searchResponse = await searchClient.search(query, { queryType: 'full', - searchFields: ['IID'], includeTotalCount: true, }); - const results: IndicatorSearchResult[] = []; + const results: BasicSearchResult[] = []; for await (const result of searchResponse.results) { results.push({ id: result?.document?.IID, diff --git a/frontend/fingertips-frontend/lib/search/searchServiceMock.ts b/frontend/fingertips-frontend/lib/search/searchServiceMock.ts new file mode 100644 index 00000000..98d97e1a --- /dev/null +++ b/frontend/fingertips-frontend/lib/search/searchServiceMock.ts @@ -0,0 +1,33 @@ +import { Search, BasicSearchResult } from './searchResultData'; + +export const MOCK_DATA: BasicSearchResult[] = [ + { + id: '1', + indicatorName: 'NHS', + latestDataPeriod: '2023', + dataSource: 'NHS website', + lastUpdated: formatDate(new Date('December 6, 2024')), + }, + { + id: '2', + indicatorName: 'DHSC', + latestDataPeriod: '2022', + dataSource: 'Student article', + lastUpdated: formatDate(new Date('November 5, 2023')), + }, +]; + +function formatDate(date: Date): string { + const day = String(date.getDate()).padStart(2, '0'); + const month = date.toLocaleString('en-GB', { month: 'long' }); + const year = date.getFullYear(); + + return `${day} ${month} ${year}`; +} + +export class SearchServiceMock implements Search { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + searchWith(indicator: string): Promise { + return Promise.resolve(MOCK_DATA); + } +} \ No newline at end of file diff --git a/frontend/fingertips-frontend/lib/util.test.ts b/frontend/fingertips-frontend/lib/util.test.ts index cee11e98..1f2bc6a3 100644 --- a/frontend/fingertips-frontend/lib/util.test.ts +++ b/frontend/fingertips-frontend/lib/util.test.ts @@ -1,5 +1,5 @@ import { expect } from '@jest/globals'; -import { shouldForwardProp } from './utils'; +import { shouldForwardProp, getEnvironmentVariable } from './utils'; describe('shouldForwardProp', () => { describe('when target is not type string', () => { @@ -31,3 +31,18 @@ describe('shouldForwardProp', () => { }); }); }); + +describe('getEnvironmentVariable', () => { + describe('if the environment is not configured', () => { + it('should throw an error on reading the missing environment variable', () => { + expect(() => {getEnvironmentVariable('MISSING_ENVIRONMENT_VARIABLE')}).toThrow(Error); + }); + }); + describe('if the environment is configured', () => { + process.env.CONFIGURED_ENVIRONMENT_VARIABLE = 'test-value'; + it('should return the value of the environment variable', () => { + const value = getEnvironmentVariable('CONFIGURED_ENVIRONMENT_VARIABLE'); + expect(value).toBe('test-value'); + }); + }); +}); \ No newline at end of file diff --git a/frontend/fingertips-frontend/lib/utils.ts b/frontend/fingertips-frontend/lib/utils.ts index ca429ccb..93560b30 100644 --- a/frontend/fingertips-frontend/lib/utils.ts +++ b/frontend/fingertips-frontend/lib/utils.ts @@ -10,3 +10,12 @@ export const shouldForwardProp = ( }; export const isBrowser = () => typeof window !== 'undefined'; + +export const getEnvironmentVariable = (name: string): string => { + return ( + process.env[name] ?? + (() => { + throw new Error(`Missing environment variable ${name}`); + })() + ); +}; From 51eb43603e774c4546a51c99e809c65d625483c6 Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Fri, 27 Dec 2024 12:55:21 +0000 Subject: [PATCH 04/10] FTN-58: wip --- frontend/fingertips-frontend/jest.config.ts | 2 + .../lib/search/searchService.test.ts | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 frontend/fingertips-frontend/lib/search/searchService.test.ts diff --git a/frontend/fingertips-frontend/jest.config.ts b/frontend/fingertips-frontend/jest.config.ts index 38fc799e..ee94354f 100644 --- a/frontend/fingertips-frontend/jest.config.ts +++ b/frontend/fingertips-frontend/jest.config.ts @@ -13,6 +13,8 @@ const config: Config = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['/jest.setup.ts'], testMatch: ['**/?(*.)+(test).[jt]s?(x)'], + transform: { '/node_modules/@azure/^.+\.(j|t)s?$': 'ts-jest' }, + transformIgnorePatterns: ['/node_modules/(?!(@azure/))'] }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/frontend/fingertips-frontend/lib/search/searchService.test.ts b/frontend/fingertips-frontend/lib/search/searchService.test.ts new file mode 100644 index 00000000..a9968e7e --- /dev/null +++ b/frontend/fingertips-frontend/lib/search/searchService.test.ts @@ -0,0 +1,39 @@ +import { SearchService } from "./searchService"; +//import { AzureKeyCredential, SearchClient } from '@azure/search-documents'; + +describe('Search-service', () => { + describe('if the environment is not configured', () => { + it('should throw an error on attempting to instantiate the service', () => { + expect(() => { new SearchService() }).toThrow(Error); + }); + }); + + describe('if the environment is configured', () => { + process.env.DHSC_AI_SEARCH_SERVICE_URL = 'test-url'; + process.env.DHSC_AI_SEARCH_INDEX_NAME = 'test-index'; + process.env.DHSC_AI_SEARCH_API_KEY = 'test-api-key'; + + jest.mock('@azure/search-documents', () => ({ + SearchClient: jest.fn().mockImplementation(() => ({ + default: jest.fn().mockImplementation(() => { + console.log('Search client construtor'); + }) + })), + // default: jest.fn().mockImplementation(() => { + + // }) + AzureKeyCredential: jest.fn().mockImplementation(() => ({ + default: jest.fn().mockImplementation(() => { + console.log('Credential construtor'); + }) + })) + })); + + + it('should return the value of the environment variable', () => { + const searchService = new SearchService(); + searchService.searchWith('test-search'); + }); + }); + +}); \ No newline at end of file From 466e45dfa652c4b201b5c15c6dc15db5bfcdac3b Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Sun, 29 Dec 2024 14:37:37 +0000 Subject: [PATCH 05/10] FTN-58: wip --- .../Search/result/searchResult.test.tsx | 2 +- .../fingertips-frontend/package-lock.json | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx b/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx index 7b832350..e382c7e8 100644 --- a/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx +++ b/frontend/fingertips-frontend/components/molecules/Search/result/searchResult.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { expect } from '@jest/globals'; -import { BasicSearchResult } from '.'; +import { SearchResult } from '.'; import { MOCK_DATA } from '@/lib/search/searchServiceMock'; import { registryWrapper } from '@/lib/testutils'; diff --git a/frontend/fingertips-frontend/package-lock.json b/frontend/fingertips-frontend/package-lock.json index 089c3732..0e141b0f 100644 --- a/frontend/fingertips-frontend/package-lock.json +++ b/frontend/fingertips-frontend/package-lock.json @@ -3203,6 +3203,21 @@ "fast-glob": "3.3.1" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.1.tgz", + "integrity": "sha512-pq7Hzu0KaaH6UYcCQ22mOuj2mWCD6iqGvYprp/Ep1EcCxbdNOSS+8EJADFbPHsaXLkaonIJ8lTKBGWXaFxkeNQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-darwin-x64": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.1.tgz", @@ -3219,6 +3234,96 @@ "node": ">= 10" } }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.1.tgz", + "integrity": "sha512-I5Q6M3T9jzTUM2JlwTBy/VBSX+YCDvPLnSaJX5wE5GEPeaJkipMkvTA9+IiFK5PG5ljXTqVFVUj5BSHiYLCpoQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.1.tgz", + "integrity": "sha512-4cPMSYmyXlOAk8U04ouEACEGnOwYM9uJOXZnm9GBXIKRbNEvBOH9OePhHiDWqOws6iaHvGayaKr+76LmM41yJA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.1.tgz", + "integrity": "sha512-KgIiKDdV35KwL9TrTxPFGsPb3J5RuDpw828z3MwMQbWaOmpp/T4MeWQCwo+J2aOxsyAcfsNE334kaWXCb6YTTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.1.tgz", + "integrity": "sha512-aHP/29x8loFhB3WuW2YaWaYFJN389t6/SBsug19aNwH+PRLzDEQfCvtuP6NxRCido9OAoExd+ZuYJKF9my1Kpg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.1.tgz", + "integrity": "sha512-klbzXYwqHMwiucNFF0tWiWJyPb45MBX1q/ATmxrMjEYgA+V/0OXc9KmNVRIn6G/ab0ASUk4uWqxik5m6wvm1sg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.1.tgz", + "integrity": "sha512-V5fm4aULqHSlMQt3U1rWAWuwJTFsb6Yh4P8p1kQFoayAF9jAQtjBvHku4zCdrtQuw9u9crPC0FNML00kN4WGhA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", From 79bebafaedc5871d35db0713b82eccd12ca44fbd Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Sun, 29 Dec 2024 14:38:35 +0000 Subject: [PATCH 06/10] FTN-58: wip --- search-setup/indexOperations.ts | 21 +++++++++++++++++++++ search-setup/package-lock.json | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/search-setup/indexOperations.ts b/search-setup/indexOperations.ts index 822dfc89..7fef6a46 100644 --- a/search-setup/indexOperations.ts +++ b/search-setup/indexOperations.ts @@ -4,6 +4,7 @@ import { SearchField, SearchClient, SearchFieldDataType, + ScoringProfile, } from "@azure/search-documents"; import { Data } from "./types.js"; @@ -41,6 +42,11 @@ function buildSearchIndex(name: string): SearchIndex { ], }, ], + scoringProfiles: [ + buildScoringProfile("IndicatorScoringProfile", "IID", 20), + buildScoringProfile("NameScoringProfile", "Descriptive/Name", 10), + buildScoringProfile("DefinitionScoringProfile", "Descriptive/Definition", 5), + ], }; } @@ -61,3 +67,18 @@ function buildSearchIndexField( hidden, }; } + +function buildScoringProfile( + name: string, + field: string, + weight: number +): ScoringProfile { + return { + name: name, + textWeights: { + weights: { + [field]: weight + } + } + }; +} diff --git a/search-setup/package-lock.json b/search-setup/package-lock.json index b50f9912..e4107074 100644 --- a/search-setup/package-lock.json +++ b/search-setup/package-lock.json @@ -1,5 +1,5 @@ { - "name": "utils", + "name": "search-setup", "lockfileVersion": 3, "requires": true, "packages": { From 753306413d00a46b43500c33beb7315973cd089f Mon Sep 17 00:00:00 2001 From: williamwillis11 <92571853+williamwillis11@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:43:32 +0000 Subject: [PATCH 07/10] fixed up jest test --- frontend/fingertips-frontend/jest.config.ts | 2 - .../lib/search/searchService.test.ts | 124 +++++++++++++----- 2 files changed, 90 insertions(+), 36 deletions(-) diff --git a/frontend/fingertips-frontend/jest.config.ts b/frontend/fingertips-frontend/jest.config.ts index ee94354f..38fc799e 100644 --- a/frontend/fingertips-frontend/jest.config.ts +++ b/frontend/fingertips-frontend/jest.config.ts @@ -13,8 +13,6 @@ const config: Config = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['/jest.setup.ts'], testMatch: ['**/?(*.)+(test).[jt]s?(x)'], - transform: { '/node_modules/@azure/^.+\.(j|t)s?$': 'ts-jest' }, - transformIgnorePatterns: ['/node_modules/(?!(@azure/))'] }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/frontend/fingertips-frontend/lib/search/searchService.test.ts b/frontend/fingertips-frontend/lib/search/searchService.test.ts index a9968e7e..9abcad4b 100644 --- a/frontend/fingertips-frontend/lib/search/searchService.test.ts +++ b/frontend/fingertips-frontend/lib/search/searchService.test.ts @@ -1,39 +1,95 @@ -import { SearchService } from "./searchService"; -//import { AzureKeyCredential, SearchClient } from '@azure/search-documents'; - -describe('Search-service', () => { - describe('if the environment is not configured', () => { - it('should throw an error on attempting to instantiate the service', () => { - expect(() => { new SearchService() }).toThrow(Error); - }); +import { SearchService } from './searchService'; +import { SearchClient, AzureKeyCredential } from '@azure/search-documents'; + +jest.mock('@azure/search-documents', () => ({ + SearchClient: jest.fn(), + AzureKeyCredential: jest.fn(), +})); + +describe('SearchService', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete process.env.DHSC_AI_SEARCH_SERVICE_URL; + delete process.env.DHSC_AI_SEARCH_INDEX_NAME; + delete process.env.DHSC_AI_SEARCH_API_KEY; + }); + + describe('if the environment is not configured it', () => { + it('should throw an error on attempting to instantiate the service', () => { + expect(() => { + new SearchService(); + }).toThrow(Error); + }); + }); + + describe('if the environment is configured it', () => { + beforeEach(() => { + process.env.DHSC_AI_SEARCH_SERVICE_URL = 'test-url'; + process.env.DHSC_AI_SEARCH_INDEX_NAME = 'test-index'; + process.env.DHSC_AI_SEARCH_API_KEY = 'test-api-key'; + + (SearchClient as jest.Mock).mockImplementation(() => ({ + search: jest.fn().mockResolvedValue({ + results: [], + }), + })); + + (AzureKeyCredential as jest.Mock).mockImplementation(() => ({ + key: 'test-api-key', + })); }); - describe('if the environment is configured', () => { - process.env.DHSC_AI_SEARCH_SERVICE_URL = 'test-url'; - process.env.DHSC_AI_SEARCH_INDEX_NAME = 'test-index'; - process.env.DHSC_AI_SEARCH_API_KEY = 'test-api-key'; - - jest.mock('@azure/search-documents', () => ({ - SearchClient: jest.fn().mockImplementation(() => ({ - default: jest.fn().mockImplementation(() => { - console.log('Search client construtor'); - }) - })), - // default: jest.fn().mockImplementation(() => { - - // }) - AzureKeyCredential: jest.fn().mockImplementation(() => ({ - default: jest.fn().mockImplementation(() => { - console.log('Credential construtor'); - }) - })) - })); - - - it('should return the value of the environment variable', () => { + it('should successfully create a search service instance', () => { const searchService = new SearchService(); - searchService.searchWith('test-search'); + expect(searchService).toBeInstanceOf(SearchService); }); - }); -}); \ No newline at end of file + it('should call search client with correct parameters', async () => { + const searchService = new SearchService(); + await searchService.searchWith('test-search'); + + expect(SearchClient).toHaveBeenCalledWith( + 'test-url', + 'test-index', + expect.any(Object) + ); + }); + + it('should perform a search operation', async () => { + const mockSearchResults = { + latestDataPeriod: undefined, + results: [ + { + document: { + IID: '123', + Descriptive: { + Name: 'Test Indicator', + DataSource: 'Test Source', + }, + DataChange: { + LastUploadedAt: '2024-01-01', + }, + }, + }, + ], + }; + + (SearchClient as jest.Mock).mockImplementation(() => ({ + search: jest.fn().mockResolvedValue(mockSearchResults), + })); + + const searchService = new SearchService(); + const results = await searchService.searchWith('test-search'); + + expect(results).toEqual([ + { + id: '123', + indicatorName: 'Test Indicator', + latestDataPeriod: undefined, + dataSource: 'Test Source', + lastUpdated: '2024-01-01', + }, + ]); + }); + }); +}); From dbf291a34d840f923a2e6a24fb4a5dd9ed6f4994 Mon Sep 17 00:00:00 2001 From: williamwillis11 <92571853+williamwillis11@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:11:54 +0000 Subject: [PATCH 08/10] code formatting --- .../fingertips-frontend/lib/search/searchServiceMock.ts | 2 +- frontend/fingertips-frontend/lib/util.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/fingertips-frontend/lib/search/searchServiceMock.ts b/frontend/fingertips-frontend/lib/search/searchServiceMock.ts index 98d97e1a..790ce0c4 100644 --- a/frontend/fingertips-frontend/lib/search/searchServiceMock.ts +++ b/frontend/fingertips-frontend/lib/search/searchServiceMock.ts @@ -30,4 +30,4 @@ export class SearchServiceMock implements Search { searchWith(indicator: string): Promise { return Promise.resolve(MOCK_DATA); } -} \ No newline at end of file +} diff --git a/frontend/fingertips-frontend/lib/util.test.ts b/frontend/fingertips-frontend/lib/util.test.ts index 1f2bc6a3..7ebbb71a 100644 --- a/frontend/fingertips-frontend/lib/util.test.ts +++ b/frontend/fingertips-frontend/lib/util.test.ts @@ -35,7 +35,9 @@ describe('shouldForwardProp', () => { describe('getEnvironmentVariable', () => { describe('if the environment is not configured', () => { it('should throw an error on reading the missing environment variable', () => { - expect(() => {getEnvironmentVariable('MISSING_ENVIRONMENT_VARIABLE')}).toThrow(Error); + expect(() => { + getEnvironmentVariable('MISSING_ENVIRONMENT_VARIABLE'); + }).toThrow(Error); }); }); describe('if the environment is configured', () => { @@ -45,4 +47,4 @@ describe('getEnvironmentVariable', () => { expect(value).toBe('test-value'); }); }); -}); \ No newline at end of file +}); From 7c40aca8fea0baf64771e961742d71cb17c3971e Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Thu, 2 Jan 2025 11:22:07 +0000 Subject: [PATCH 09/10] FTN-58: added error handling --- .../app/search/results/page.tsx | 16 +- .../error/__snapshots__/index.test.tsx.snap | 246 ++++++++++++++++++ .../components/pages/error/index.test.tsx | 31 +++ .../components/pages/error/index.tsx | 29 +++ 4 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 frontend/fingertips-frontend/components/pages/error/__snapshots__/index.test.tsx.snap create mode 100644 frontend/fingertips-frontend/components/pages/error/index.test.tsx create mode 100644 frontend/fingertips-frontend/components/pages/error/index.tsx diff --git a/frontend/fingertips-frontend/app/search/results/page.tsx b/frontend/fingertips-frontend/app/search/results/page.tsx index e505ef35..c1a9967b 100644 --- a/frontend/fingertips-frontend/app/search/results/page.tsx +++ b/frontend/fingertips-frontend/app/search/results/page.tsx @@ -1,4 +1,5 @@ import { SearchResults } from '@/components/pages/search/results'; +import { ErrorPage } from '@/components/pages/error'; import { getSearchService } from '@/lib/search/searchResultData'; export default async function Page( @@ -11,8 +12,15 @@ export default async function Page( const searchParams = await props.searchParams; const indicator = searchParams?.indicator ?? ''; - // Perform async API call using indicator prop - const searchResults = await getSearchService().searchWith(indicator); - - return ; + try { + // Perform async API call using indicator prop + const searchResults = await getSearchService().searchWith(indicator); + return ; + } + catch(error) { + // Log error response + // TBC + console.log(`Error response received from call to Search service: ${error}`); + return ; + } } diff --git a/frontend/fingertips-frontend/components/pages/error/__snapshots__/index.test.tsx.snap b/frontend/fingertips-frontend/components/pages/error/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..f6645d2c --- /dev/null +++ b/frontend/fingertips-frontend/components/pages/error/__snapshots__/index.test.tsx.snap @@ -0,0 +1,246 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Page snapshot test 1`] = ` + + .c0 { + color: #0b0c0c; + font-family: "nta",Arial,sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 700; + font-size: 32px; + line-height: 1.09375; + display: block; + margin-top: 0; + margin-bottom: 30px; +} + +@media print { + .c0 { + color: #000; + } +} + +@media print { + .c0 { + font-size: 32px; + line-height: 1.15; + } +} + +@media only screen and (min-width: 641px) { + .c0 { + font-size: 48px; + line-height: 1.0416666666666667; + } +} + +@media only screen and (min-width: 641px) { + .c0 { + margin-bottom: 50px; + } +} + +@media print { + +} + +@media print { + +} + +@media print { + +} + +@media only screen and (min-width: 641px) { + +} + +@media only screen and (min-width: 641px) { + +} + +

+ An error has occurred. +

+ .c0 { + margin: 0; + font-family: "nta",Arial,sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + font-size: 16px; + line-height: 1.25; + margin-bottom: 15px; +} + +.c0 >p { + margin: 0; +} + +.c0 >p>code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; +} + +.c0 >pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; +} + +.c0 >pre>code { + display: inline; + padding: 0; + margin: 0; + border: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; +} + +@media print { + +} + +@media print { + +} + +@media only screen and (min-width: 641px) { + +} + +@media only screen and (min-width: 641px) { + +} + +@media print { + +} + +@media print { + +} + +@media print { + .c0 { + font-size: 14px; + line-height: 1.15; + } +} + +@media only screen and (min-width: 641px) { + .c0 { + font-size: 19px; + line-height: 1.3157894736842106; + } +} + +@media only screen and (min-width: 641px) { + .c0 { + margin-bottom: 20px; + } +} + +
+

+ test error text +

+
+ .c0 { + font-family: "nta",Arial,sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.c0:focus { + outline: 3px solid #ffdd00; + outline-offset: 0; + background-color: #ffdd00; +} + +.c0 :link { + color: #1d70b8; +} + +.c0 :visited { + color: #4c2c92; +} + +.c0 :hover { + color: #2b8cc4; +} + +.c0 :active { + color: #2b8cc4; +} + +.c0 :focus { + color: #0b0c0c; +} + +@media print { + +} + +@media print { + +} + +@media only screen and (min-width: 641px) { + +} + +@media only screen and (min-width: 641px) { + +} + +@media print { + .c0 { + font-family: sans-serif; + } +} + +@media print { + .c0[href^="/"]::after, + .c0[href^="http://"]::after, + .c0[href^="https://"]::after { + content: " (" attr(href) ")"; + font-size: 90%; + word-wrap: break-word; + } +} + +@media print { + +} + +@media only screen and (min-width: 641px) { + +} + +@media only screen and (min-width: 641px) { + +} + + + test error link text + +
+`; diff --git a/frontend/fingertips-frontend/components/pages/error/index.test.tsx b/frontend/fingertips-frontend/components/pages/error/index.test.tsx new file mode 100644 index 00000000..99960ed6 --- /dev/null +++ b/frontend/fingertips-frontend/components/pages/error/index.test.tsx @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react'; +import { expect } from '@jest/globals'; +import { ErrorPage } from '.'; +import { registryWrapper } from '@/lib/testutils'; + +describe('Error Page', () => { + const errorText = 'test error text'; + const errorLink = '/test-link'; + const errorLinkText = 'test error link text'; + + test('should render elements', async () => { + render( + registryWrapper( + + ) + ); + + expect(screen.getByText(/test error text/i)).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'test error link text' })).toHaveAttribute('href', '/test-link'); + }); + + test('snapshot test', () => { + const container = render( + registryWrapper( + + ) + ); + + expect(container.asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/fingertips-frontend/components/pages/error/index.tsx b/frontend/fingertips-frontend/components/pages/error/index.tsx new file mode 100644 index 00000000..09aa8de5 --- /dev/null +++ b/frontend/fingertips-frontend/components/pages/error/index.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { + H1, + Link, + Paragraph, +} from 'govuk-react'; + +type ErrorPageProps = { + errorText: string; + errorLink: string; + errorLinkText: string; +}; + +export function ErrorPage({ + errorText, + errorLink, + errorLinkText +}: Readonly) { + return ( + <> +

An error has occurred.

+ {`${errorText}`} + + {`${errorLinkText}`} + + + ); +} From a2d3ae1c5ee9901cf3143b8d88b51d66548fb68e Mon Sep 17 00:00:00 2001 From: Alan Taylor Date: Thu, 2 Jan 2025 17:01:10 +0000 Subject: [PATCH 10/10] FTN-58: update to scoring profile --- search-setup/indexOperations.ts | 28 +++++++++++++++++------ search-setup/sample-data.ts | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/search-setup/indexOperations.ts b/search-setup/indexOperations.ts index 7fef6a46..c788b327 100644 --- a/search-setup/indexOperations.ts +++ b/search-setup/indexOperations.ts @@ -8,6 +8,10 @@ import { } from "@azure/search-documents"; import { Data } from "./types.js"; +interface ScoringWeight { + [fieldName: string]: number; +} + export async function createSearchIndex( indexClient: SearchIndexClient, indexName: string @@ -43,9 +47,14 @@ function buildSearchIndex(name: string): SearchIndex { }, ], scoringProfiles: [ - buildScoringProfile("IndicatorScoringProfile", "IID", 20), - buildScoringProfile("NameScoringProfile", "Descriptive/Name", 10), - buildScoringProfile("DefinitionScoringProfile", "Descriptive/Definition", 5), + buildScoringProfile( + "BasicScoringProfile", + [ + { "IID": 20 }, + { "Descriptive/Name": 10 }, + { "Descriptive/Definition": 5 }, + ] + ) ], }; } @@ -70,15 +79,20 @@ function buildSearchIndexField( function buildScoringProfile( name: string, - field: string, - weight: number + weights: ScoringWeight[] ): ScoringProfile { - return { + let scoringProfile: ScoringProfile = + { name: name, textWeights: { weights: { - [field]: weight } } }; + + for (const weighting of weights) { + scoringProfile.textWeights!.weights = { ...scoringProfile.textWeights!.weights, ...weighting }; + } + + return scoringProfile; } diff --git a/search-setup/sample-data.ts b/search-setup/sample-data.ts index e0c3ecae..df88a090 100644 --- a/search-setup/sample-data.ts +++ b/search-setup/sample-data.ts @@ -40,4 +40,43 @@ export const sampleData: Data[] = [ "The percentage of patients with stroke or transient ischaemic attack (TIA), as recorded on practice disease registers (proportion of total list size).", }, }, + { + IID: "300", + Descriptive: { + Name: "Participation rate, total", + Definition: + "Proportion of eligible children measured in the National Child Measurement Programme (NCMP)", + }, + }, + { + IID: "310", + Descriptive: { + Name: "Rate of admissions due to liver disease in per 300,000 population", + Definition: "Directly age-standardised rate of hospital admissions due to liver disease in per 300,000 population", + }, + }, + { + IID: "316", + Descriptive: { + Name: "Study of of drug sensitive TB notifications across full course of treatment", + Definition: + "The annual proportion of drug sensitive TB notifications expected to complete treatment within 310 days of treatment start date", + }, + }, + { + IID: "327", + Descriptive: { + Name: "Waiting < 310 days to enter IAPT treatment ", + Definition: + "Study across 316 hospitals of the number of service requests with first treatment appointment in the month where the individual had a significant wait", + }, + }, + { + IID: "330", + Descriptive: { + Name: "Waiting > 310 days to enter IAPT treatment ", + Definition: + "Study of the number of service requests with first treatment appointment in the month where the individual had a > 310 day wait", + }, + }, ];