Skip to content
Merged
113 changes: 59 additions & 54 deletions materials_devops_pipelines/Materials-PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,67 +226,72 @@ stages:
VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL)
VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE)
VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL)
VITE_BASE_URL: $(BASE_URL)
VITE_E2E: True
VITE_REDACTION_LOG_URL: $(VITE_REDACTION_LOG_URL)
VITE_REDACTION_LOG_SCOPE: $(VITE_REDACTION_LOG_SCOPE)

# ---------- E2E TESTS (Playwright) ----------

#temp removal of E2E tests until pipeline issues resolved
# - script: |
# npx playwright install
# displayName: 'Install Playwright browsers'
# condition: ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true')
# workingDirectory: '$(workingDir)'
# env:
# PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'
- script: |
npx playwright install
displayName: 'Install Playwright browsers'
condition: ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true')
workingDirectory: '$(workingDir)'
env:
PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'

- script: |
npx playwright install-deps
displayName: 'Install Playwright OS dependencies'
condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true'))
workingDirectory: '$(workingDir)'
env:
PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'

# - script: |
# npx playwright install-deps
# displayName: 'Install Playwright OS dependencies'
# condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true'))
# workingDirectory: '$(workingDir)'
# env:
# PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'
- script: |
npx playwright install msedge
displayName: 'Install Microsoft Edge (Playwright channel browser)'
condition: eq(variables['Agent.OS'], 'Linux')
workingDirectory: '$(workingDir)'
env:
PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'

# - task: Npm@1
# displayName: 'Run E2E Tests with Coverage'
# inputs:
# command: 'custom'
# customCommand: 'run e2e'
# workingDir: '$(workingDir)'
# env:
# PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'
# E2E_TEST_MS_USERNAME: $(e2e_username)
# E2E_TEST_MS_PASSWORD: $(e2e_password)
# E2E_CIN3_USERNAME: $(e2e_cin3_username)
# E2E_CIN3_PASSWORD: $(e2e_cin3_password)
# E2E_CMS_COOKIE_URL: $(e2e_cms_cookie_url)
# E2E_URN: $(e2e_urn)
# E2E_CASE: $(e2e_case)
# VITE_MSAL_CLIENT_ID: $(MSAL_CLIENT_ID)
# VITE_MSAL_TENANT_ID: $(MSAL_TENANT_ID)
# VITE_MSAL_REDIRECT_URI: $(MSAL_REDIRECT_URI_LOCAL)
# VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL)
# VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE)
# VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL)
# VITE_BASE_URL: $(BASE_URL)
# VITE_E2E: True
- task: Npm@1
displayName: 'Run E2E Tests with Coverage'
inputs:
command: 'custom'
customCommand: 'run e2e'
workingDir: '$(workingDir)'
env:
PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright'
E2E_TEST_MS_USERNAME: $(e2e_username)
E2E_TEST_MS_PASSWORD: $(e2e_password)
E2E_CIN3_USERNAME: $(e2e_cin3_username)
E2E_CIN3_PASSWORD: $(e2e_cin3_password)
E2E_CMS_COOKIE_URL: $(e2e_cms_cookie_url)
E2E_URN: $(e2e_urn)
E2E_CASE: $(e2e_case)
VITE_MSAL_CLIENT_ID: $(MSAL_CLIENT_ID)
VITE_MSAL_TENANT_ID: $(MSAL_TENANT_ID)
VITE_MSAL_REDIRECT_URI: $(MSAL_REDIRECT_URI_LOCAL)
VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL)
VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE)
VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL)
VITE_E2E: True

# - task: PublishPipelineArtifact@1
# displayName: 'Publish E2E Artifact'
# inputs:
# targetPath: '$(workingDir)/tests/playwright-report'
# artifact: 'playwright-report'
# publishLocation: 'pipeline'
# condition: succeededOrFailed()
- task: PublishPipelineArtifact@1
displayName: 'Publish E2E Artifact'
inputs:
targetPath: '$(workingDir)/tests/playwright-report'
artifact: 'playwright-report'
publishLocation: 'pipeline'
condition: succeededOrFailed()

# - task: PublishTestResults@2
# displayName: 'Publish E2E Test Results (JUnit)'
# inputs:
# testResultsFormat: 'JUnit'
# testResultsFiles: '$(workingDir)/tests/e2e-test-results.xml'
# testRunTitle: 'E2E Tests'
# publishRunAttachments: false
# condition: always()
- task: PublishTestResults@2
displayName: 'Publish E2E Test Results (JUnit)'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(workingDir)/tests/e2e-test-results.xml'
testRunTitle: 'E2E Tests'
publishRunAttachments: false
condition: always()
13 changes: 11 additions & 2 deletions materials_ui/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { defineConfig, devices } from '@playwright/test';

const e2eUrn = process.env.E2E_URN;
const e2eCase = process.env.E2E_CASE;
const e2eRoute = e2eUrn && e2eCase ? `${e2eUrn}/${e2eCase}/` : '';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
Expand Down Expand Up @@ -29,7 +33,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: `http://localhost:3000/materials-ui/${process.env.E2E_URN}/${process.env.E2E_CASE}/`,
baseURL: `http://localhost:3000/materials-ui/${e2eRoute}`,
video: 'retain-on-failure',
screenshot: 'only-on-failure',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
Expand Down Expand Up @@ -87,6 +91,11 @@ export default defineConfig({
command: 'npm run dev',
url: 'http://localhost:3000/materials-ui',
reuseExistingServer: !process.env.CI,
timeout: 120_000
timeout: 120_000,
env: {
...process.env,
// Keep E2E independent from external script availability in CI.
VITE_GLOBAL_SCRIPT_URL: ''
}
}
});
6 changes: 4 additions & 2 deletions materials_ui/src/components/CaseInfo/CaseInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export const CaseInfo = ({ caseInfo }: Props) => {
{caseInfoName}
</h2>
<p className="govuk-body caseInfo__urn">{caseInfo?.urn}</p>
{caseInfo.numberOfDefendants > 1 && (
{caseInfo.numberOfDefendants > 0 && (
<p style={{ marginTop: 0 }}>
<a href="#" onClick={handleCaseDefendantsLinkClick}>
View {caseInfo.numberOfDefendants} defendants and charges
{caseInfo.numberOfDefendants === 1
? 'View defendant and charges'
: `View ${caseInfo.numberOfDefendants} defendants and charges`}
</a>
</p>
)}
Expand Down
31 changes: 10 additions & 21 deletions materials_ui/src/components/DocumentPreview/DocumentPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Banner } from '../../components';
import { useDocumentPreview } from '../../hooks/';
import { CaseMaterialsType } from '../../schemas/caseMaterials';
import { ErrorSummary } from '../ErrorSummary/ErrorSummary';
import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
import { PdfViewer } from '../PdfViewer/PdfViewer';
import { Banner } from '../../components';

type Props = { row: CaseMaterialsType };

Expand All @@ -14,36 +14,25 @@ export default function DocumentPreview({ row }: Props) {
error: caseDocumentError
} = useDocumentPreview({ materialId: row.materialId });

const errorTitle = caseDocumentError?.toString().includes('403') ?
'This document is password protected' : 'No preview available';
const errorTitle = caseDocumentError?.toString().includes('403')
? 'This document is password protected'
: 'There is a problem';

const errorMessage = caseDocumentError?.toString().includes('403') ?
'Ask the agency who supplied it to remove the password and resend the document.' :
'The material you are trying to preview is not currently available. You can view it in CMS.';
const errorMessage = caseDocumentError?.toString().includes('403')
? 'Ask the agency who supplied it to remove the password and resend the document.'
: 'This document cannot be shown. You can still view it in CMS.';

let content = null;

if (!caseDocumentLoading) {
if (caseDocumentError) {
content = caseDocumentError.toString().includes('403') ? (
<Banner
type="error"
header={errorTitle}
content={errorMessage}
/>
<Banner type="error" header={errorTitle} content={errorMessage} />
) : (
<ErrorSummary
errorTitle={errorTitle}
errorMessage={errorMessage}
/>
<ErrorSummary errorTitle={errorTitle} errorMessage={errorMessage} />
);
} else if (caseDocumentData) {
content = (
<PdfViewer
file={caseDocumentData}
fileName={row.subject}
/>
);
content = <PdfViewer file={caseDocumentData} fileName={row.subject} />;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ export type TBulkSearchInternalState =
| { status: 'done'; candidates: TRedaction[]; focusedIndex: number }
| { status: 'error' };

const POLL_INTERVAL_MS = 3000;
const MAX_SEARCH_ATTEMPTS = 5;

const wait = (ms: number, signal: AbortSignal) =>
new Promise<void>((resolve) => {
const timeoutId = setTimeout(resolve, ms);
signal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId);
resolve();
},
{ once: true }
);
});

export const useBulkSearch = (p: {
axiosInstance: AxiosInstance;
urn: string;
Expand All @@ -34,24 +50,44 @@ export const useBulkSearch = (p: {
const controller = new AbortController();
abortRef.current = controller;
setState({ status: 'loading' });

try {
const resp = await bulkSearchDocument({
axiosInstance: p.axiosInstance,
urn: p.urn,
caseId: p.caseId,
versionId: p.versionId,
documentId: p.documentId,
searchText,
signal: controller.signal
});
if (controller.signal.aborted) return undefined;
if (resp.isNotFound || resp.failedReason) {
setState({ status: 'error' });
return undefined;
for (let attempt = 1; attempt <= MAX_SEARCH_ATTEMPTS; attempt++) {
const { status, data } = await bulkSearchDocument({
axiosInstance: p.axiosInstance,
urn: p.urn,
caseId: p.caseId,
versionId: p.versionId,
documentId: p.documentId,
searchText,
signal: controller.signal
});
if (controller.signal.aborted) return undefined;

if (status === 200) {
if (!data || data.isNotFound || data.failedReason) {
setState({ status: 'error' });
return undefined;
}
const candidates = convertSearchResponseToRedactions(data);
setState({ status: 'done', candidates, focusedIndex: 0 });
return candidates;
}

const stillProcessing = status === 202 || status === 423;
if (!stillProcessing) {
setState({ status: 'error' });
return undefined;
}

if (attempt < MAX_SEARCH_ATTEMPTS) {
await wait(POLL_INTERVAL_MS, controller.signal);
if (controller.signal.aborted) return undefined;
}
}
const candidates = convertSearchResponseToRedactions(resp);
setState({ status: 'done', candidates, focusedIndex: 0 });
return candidates;

setState({ status: 'error' });
return undefined;
} catch (err) {
if (axios.isCancel(err)) return undefined;
setState({ status: 'error' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export type TBulkSearchResponse = {
isNotFound: boolean;
};

export type TBulkSearchResult = {
status: number;
data: TBulkSearchResponse | null;
};

export const bulkSearchDocument = async (p: {
axiosInstance: AxiosInstance;
urn: string;
Expand All @@ -27,10 +32,14 @@ export const bulkSearchDocument = async (p: {
documentId: string;
searchText: string;
signal?: AbortSignal;
}): Promise<TBulkSearchResponse> => {
const response = await p.axiosInstance.get(
}): Promise<TBulkSearchResult> => {
const response = await p.axiosInstance.get<TBulkSearchResponse>(
`/api/urns/${p.urn}/cases/${p.caseId}/documents/${p.documentId}/versions/${p.versionId}/search`,
{ params: { SearchText: p.searchText }, signal: p.signal }
{
params: { SearchText: p.searchText },
signal: p.signal,
validateStatus: () => true
}
);
return response.data;
return { status: response.status, data: response.data ?? null };
};
Loading
Loading