diff --git a/source/frontend/src/components/common/ExpandableTextArea.tsx b/source/frontend/src/components/common/ExpandableTextArea.tsx index 2f8db56c7c..bc9285def6 100644 --- a/source/frontend/src/components/common/ExpandableTextArea.tsx +++ b/source/frontend/src/components/common/ExpandableTextArea.tsx @@ -7,6 +7,7 @@ import { LinkButton } from './buttons'; export interface IExpandableTextCardProps { text: string; maxHeight?: number; + dataTestid?: string; } export const ExpandableTextCard: React.FunctionComponent< @@ -26,7 +27,7 @@ export const ExpandableTextCard: React.FunctionComponent< }, [max]); return ( - + diff --git a/source/frontend/src/features/documents/documentUpload/SelectedDocumentRow.tsx b/source/frontend/src/features/documents/documentUpload/SelectedDocumentRow.tsx index b03c1794d4..295fef69e3 100644 --- a/source/frontend/src/features/documents/documentUpload/SelectedDocumentRow.tsx +++ b/source/frontend/src/features/documents/documentUpload/SelectedDocumentRow.tsx @@ -111,7 +111,12 @@ export const SelectedDocumentRow: React.FunctionComponent - {fileError} + + {fileError} + } isCollapsable={false} diff --git a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap index 9c4a5c7ef7..5de0001ba1 100644 --- a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap @@ -1379,6 +1379,7 @@ exports[`ResearchContainer component > renders as expected 1`] = `
renders as expected 1`] = `
renders as expected 1`] = `
matches snapshot 1`] = `
matches snapshot 1`] = `
matches snapshot 1`] = `
= )} - +
@@ -135,12 +138,15 @@ const ResearchSummaryView: React.FunctionComponent = : 'not complete'} - +
{detail.isExpropriation ? 'Yes' : 'No'} - +
); diff --git a/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/details/__snapshots__/ResearchSummaryView.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/details/__snapshots__/ResearchSummaryView.test.tsx.snap index 521c6792f1..923c3d59cf 100644 --- a/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/details/__snapshots__/ResearchSummaryView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/tabs/fileDetails/details/__snapshots__/ResearchSummaryView.test.tsx.snap @@ -278,6 +278,7 @@ exports[`ResearchSummaryView component > renders as expected 1`] = `
renders as expected 1`] = `
renders as expected 1`] = `
| null; + //fileProperties: ApiGen_Concepts_FileProperty[] | null; + roadName: string | null; + roadAlias: string | null; + //fileProperties: ApiGen_Concepts_ResearchFileProperty[] | null; + //requestDate: UtcIsoDate | null; + requestDescription: string | null; + requestSourceDescription: string | null; + researchResult: string | null; + //researchCompletionDate: UtcIsoDate | null; + isExpropriation: boolean | null; + expropriationNotes: string | null; + //requestSourceType: ApiGen_Base_CodeType | null; + //requestorPerson: ApiGen_Concepts_Person | null; + //requestorOrganization: ApiGen_Concepts_Organization | null; + //researchFilePurposes: ApiGen_Concepts_ResearchFilePurpose[] | null; + //researchFileProjects: ApiGen_Concepts_ResearchFileProject[] | null; + //appCreateTimestamp: UtcIsoDateTime; + //appLastUpdateTimestamp: UtcIsoDateTime; + appLastUpdateUserid: string | null; + appCreateUserid: string | null; + appLastUpdateUserGuid: string | null; + appCreateUserGuid: string | null; + rowVersion: number | null; +} diff --git a/testing/pims-integration/pages/documents/document-upload-modal.page.ts b/testing/pims-integration/pages/documents/document-upload-modal.page.ts index dee52bc9f3..0b04e841bd 100644 --- a/testing/pims-integration/pages/documents/document-upload-modal.page.ts +++ b/testing/pims-integration/pages/documents/document-upload-modal.page.ts @@ -36,10 +36,8 @@ export class DocumentUploadModalPage { } async getDocumentErrorLabel(index: number = 0): Promise { - const documentHeaderWrapper = this.page.locator( - `//div[@data-testid='document[${index}]-error']/parent::div/following-sibling::div` - ); - return await documentHeaderWrapper.innerText(); + const documentErrorLabel = this.page.getByTestId(`document[${index}]-error-message`); + return await documentErrorLabel.innerText(); } async getDocumentErrorFilename(index: number = 0): Promise { diff --git a/testing/pims-integration/pages/research/research-create.page.ts b/testing/pims-integration/pages/research/research-create.page.ts index c59af2dadd..fd173caa4f 100644 --- a/testing/pims-integration/pages/research/research-create.page.ts +++ b/testing/pims-integration/pages/research/research-create.page.ts @@ -71,22 +71,6 @@ export class ResearchCreatePage extends LayoutPage { await this.page.goto('/mapview/sidebar/research/new', { waitUntil: 'domcontentloaded' }); } - async setResearchNameValue(researchName: string) { - const now = new Date(); - - const timestamp = - now.getFullYear().toString() + - String(now.getMonth() + 1).padStart(2, '0') + - String(now.getDate()).padStart(2, '0') + - String(now.getHours()).padStart(2, '0') + - String(now.getMinutes()).padStart(2, '0') + - String(now.getSeconds()).padStart(2, '0'); - - const fileName = `${researchName}-${timestamp}`; - - await this.researchNameInput.fill(fileName); - } - async cancelButtonClick() { await this.cancelButton.click(); } diff --git a/testing/pims-integration/pages/research/research-list.page.ts b/testing/pims-integration/pages/research/research-list.page.ts index b802d294ed..e878258c86 100644 --- a/testing/pims-integration/pages/research/research-list.page.ts +++ b/testing/pims-integration/pages/research/research-list.page.ts @@ -13,6 +13,7 @@ export class ResearchListPage extends LayoutPage { readonly researcSearchByLabel: Locator; readonly researchByRegionSelect: Locator; + readonly researchSearchNameInput: Locator; readonly researchByStatusSelect: Locator; readonly researchSearchBySelect: Locator; readonly researchSearchPidInput: Locator; @@ -74,6 +75,7 @@ export class ResearchListPage extends LayoutPage { this.researchByStatusSelect = page.locator('#input-researchFileStatusTypeCode'); this.researchSearchBySelect = page.locator('#input-researchSearchBy'); this.researchSearchPidInput = page.locator('#input-pid'); + this.researchSearchNameInput = page.locator('#input-name'); this.researchSearchRoadInput = page.locator('#input-roadOrAlias'); this.researchSearchDateSelect = page.locator('#input-createOrUpdateRange'); this.researchSearchDateToInput = page.locator('#datepicker-updatedOnStartDate'); @@ -134,4 +136,42 @@ export class ResearchListPage extends LayoutPage { async createNewResearchClick() { await this.researchNewButton.click(); } + + async openResearchFileInNewTab(index: number): Promise { + const selectedFile = this.page + .getByTestId('researchFilesTable') + .locator('.tbody .tr-wrapper') + .nth(index - 1) + .locator('a'); + const [newPage] = await Promise.all([ + this.page.context().waitForEvent('page'), + selectedFile.click(), + ]); + + await newPage.waitForLoadState('domcontentloaded'); + await newPage.waitForURL(/\/mapview\/sidebar\/research\/\d+/, { + timeout: 30000, + }); + // eslint-disable-next-line playwright/no-networkidle + await newPage.waitForLoadState('networkidle'); + await newPage.bringToFront(); + + return newPage; + } + + async searchByName(name: string) { + await this.researchSearchBySelect.selectOption('Research file name'); + await this.researchSearchNameInput.fill(name); + await this.researchSearchButton.click(); + } + + async clickResearchFileResult(index: number): Promise { + const link = this.page + .getByTestId('researchFilesTable') + .locator('.tbody .tr-wrapper') + .nth(index - 1) + .locator('a'); + + await link.click(); + } } diff --git a/testing/pims-integration/pages/research/research-view-file-details.page.ts b/testing/pims-integration/pages/research/research-view-file-details.page.ts index 658198b9c1..344170fdb4 100644 --- a/testing/pims-integration/pages/research/research-view-file-details.page.ts +++ b/testing/pims-integration/pages/research/research-view-file-details.page.ts @@ -10,25 +10,20 @@ export class ResearchViewFileDetails extends LayoutPage { readonly researchProjectSubtitle: Locator; readonly researchProjectLabel: Locator; - readonly researchProjectContent: Locator; readonly reseachRoadTitle: Locator; readonly researchRoadNameLabel: Locator; - readonly researchRoadNameContent: Locator; readonly researchRoadAliasLabel: Locator; - readonly researchRoadAliasContent: Locator; readonly researchRequestTitle: Locator; readonly researchPurposeLabel: Locator; - readonly researchPurposeContent: Locator; readonly researchRequestDateLabel: Locator; readonly researchSourceRequestLabel: Locator; - readonly researchRequestDateContent: Locator; + readonly researchRequesterLabel: Locator; readonly researchSourceRequestContent: Locator; readonly researchRequesterContent: Locator; readonly researchDescriptionLabel: Locator; - readonly researchDescriptionContent: Locator; readonly researchResultTitle: Locator; readonly researchCompletedOnLabel: Locator; @@ -48,29 +43,17 @@ export class ResearchViewFileDetails extends LayoutPage { this.researchProjectSubtitle = page.locator('div').filter({ hasText: 'Project' }).first(); this.researchProjectLabel = page.getByText('Ministry project:', { exact: true }); - this.researchProjectContent = page.locator( - "//label[text()='Ministry project']/parent::div/following-sibling::div" - ); this.reseachRoadTitle = page.getByText('Roads', { exact: true }); this.researchRoadNameLabel = page.locator('label:has-text("Road name:")'); - this.researchRoadNameContent = page.locator( - "//label[text()='Road name:']/parent::div/following-sibling::div" - ); + this.researchRoadAliasLabel = page.locator('label:has-text("Road alias:")'); - this.researchRoadAliasContent = page.locator( - "//label[text()='Road alias:']/parent::div/following-sibling::div" - ); this.researchRequestTitle = page.getByText('Research Request', { exact: true }); this.researchPurposeLabel = page.getByText('Research purpose:', { exact: true }); - this.researchPurposeContent = page.locator( - "//label[text()='Research purpose:']/parent::div/following-sibling::div" - ); + this.researchRequestDateLabel = page.getByText('Request date:', { exact: true }); - this.researchRequestDateContent = page.locator( - "//label[text()='Request date:']/parent::div/following-sibling::div" - ); + this.researchSourceRequestLabel = page.getByText('Source of request:', { exact: true }); this.researchSourceRequestContent = page.locator( "//label[text()='Source of request:']/parent::div/following-sibling::div" @@ -80,9 +63,6 @@ export class ResearchViewFileDetails extends LayoutPage { "//label[text()='Source of request:']/parent::div/following-sibling::div" ); this.researchDescriptionLabel = page.locator('label:has-text("Requester:")'); - this.researchDescriptionContent = page.locator( - "//label[text()='Requester:']/parent::div/following-sibling::div" - ); this.researchResultTitle = page.locator(':text-is("Result")'); this.researchCompletedOnLabel = page.locator('label:has-text("Research completed on:")'); @@ -96,4 +76,24 @@ export class ResearchViewFileDetails extends LayoutPage { async navigateDocumentsTab() { await this.researchDocumentTab.click(); } + + async getFieldValueByLabel(label: string): Promise { + return await this.page + .locator( + `//label[contains(normalize-space(), '${label}')]/parent::div/following-sibling::div` + ) + .innerText(); + } + + async getResearchDescription(): Promise { + return (await this.page.getByTestId('request-description').locator('label').innerText()).trim(); + } + + async getResearchResult(): Promise { + return (await this.page.getByTestId('research-result').locator('label').innerText()).trim(); + } + + async getResearchExpropriationNotes(): Promise { + return (await this.page.getByTestId('expropriation-notes').locator('label').innerText()).trim(); + } } diff --git a/testing/pims-integration/tests/e2e/research.spec.ts b/testing/pims-integration/tests/e2e/research.spec.ts deleted file mode 100644 index ed3de304ad..0000000000 --- a/testing/pims-integration/tests/e2e/research.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import test, { BrowserContext, expect, Page } from '@playwright/test'; -import { ResearchCreatePage } from '../../pages/research/research-create.page'; -import { DocumentsListPage } from '../../pages/documents/documents-list.page'; -import { ResearchViewFileDetails } from '../../pages/research/research-view-file-details.page'; -import { DocumentUploadModalPage } from '../../pages/documents/document-upload-modal.page'; -import path from 'path'; - -let context: BrowserContext; -let page: Page; -let researchCreatePage: ResearchCreatePage; -let researchViewDetails: ResearchViewFileDetails; -let documentsListPage: DocumentsListPage; -let documentUploadModalPage: DocumentUploadModalPage; - -test.describe('Research Files feature', () => { - test.beforeAll(async ({ browser }) => { - context = await browser.newContext(); - page = await context.newPage(); - researchCreatePage = new ResearchCreatePage(page); - researchViewDetails = new ResearchViewFileDetails(page); - documentsListPage = new DocumentsListPage(page); - documentUploadModalPage = new DocumentUploadModalPage(page); - }); - - test.afterAll(async () => { - await context.close(); - }); - - test('verify documents invalid types', async () => { - //Navigate to new research file and create a minimum viable research - await researchCreatePage.goto(); - await researchCreatePage.setResearchNameValue('researchFile'); - await researchCreatePage.confirmButtonClick(); - await expect(researchViewDetails.researchDocumentTab).toBeVisible(); - - //Navigate to the Document tab and create a new document - await researchViewDetails.navigateDocumentsTab(); - await documentsListPage.addDocumentButtonClick(); - const documentTypes = await documentUploadModalPage.getSupportedFileExtensionsText(); - expect(documentTypes).toBe( - 'Supported file types include txt, pdf, docx, doc, xlsx, xls, html, odt, png, jpg, bmp, tif, tiff, jpeg, gif, shp, gml, kml, kmz, msg.' - ); - - //Insert an invalid document type - const filePath = path.resolve(process.cwd(), 'fixtures', 'react-icon.svg'); - await documentUploadModalPage.uploadDocument(filePath); - - //Verify warning message appears - const errorLabel = await documentUploadModalPage.getDocumentErrorLabel(); - expect(errorLabel).toBe('File type not supported!'); - - const fileName = await documentUploadModalPage.getDocumentErrorFilename(); - expect(fileName).toBe('react-icon.svg'); - }); -}); diff --git a/testing/pims-integration/tests/e2e/research/research-list-view.spec.ts b/testing/pims-integration/tests/e2e/research/research-list-view.spec.ts new file mode 100644 index 0000000000..b23e5ad9f5 --- /dev/null +++ b/testing/pims-integration/tests/e2e/research/research-list-view.spec.ts @@ -0,0 +1,24 @@ +import test, { BrowserContext, expect, Page } from '@playwright/test'; +import { ResearchListPage } from '../../../pages/research/research-list.page'; + +let context: BrowserContext; +let page: Page; +let researchListPage: ResearchListPage; + +test.describe('Research Files feature', () => { + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + researchListPage = new ResearchListPage(page); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test('verify research list view', async () => { + //Navigate to research list view and pick an existing research file + await researchListPage.goto(); + await expect(researchListPage.researchListTitle).toBeVisible(); + }); +}); diff --git a/testing/pims-integration/tests/e2e/research/research.spec.ts b/testing/pims-integration/tests/e2e/research/research.spec.ts new file mode 100644 index 0000000000..ebad39bfb6 --- /dev/null +++ b/testing/pims-integration/tests/e2e/research/research.spec.ts @@ -0,0 +1,128 @@ +import test, { BrowserContext, expect, Page } from '@playwright/test'; +import { ResearchCreatePage } from '../../../pages/research/research-create.page'; +import { DocumentsListPage } from '../../../pages/documents/documents-list.page'; + +import { ResearchViewFileDetails } from '../../../pages/research/research-view-file-details.page'; +import { DocumentUploadModalPage } from '../../../pages/documents/document-upload-modal.page'; +import { generateFileName, normalize, formatApiDate, formatApiBoolean } from '../../../utils/utils'; +import path from 'path'; + +let context: BrowserContext; +let page: Page; + +let researchCreatePage: ResearchCreatePage; +let researchViewDetails: ResearchViewFileDetails; +let documentsListPage: DocumentsListPage; +let documentUploadModalPage: DocumentUploadModalPage; + +test.describe('Research Files feature', () => { + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + researchCreatePage = new ResearchCreatePage(page); + researchViewDetails = new ResearchViewFileDetails(page); + documentsListPage = new DocumentsListPage(page); + documentUploadModalPage = new DocumentUploadModalPage(page); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test('new research file', async () => { + let apiFeatureFileJson: { + isExpropriation: boolean | null; + expropriationNotes: string; + researchResult: string; + requestDescription: string; + researchFileProjects: any[]; + roadName: any; + roadAlias: any; + researchFilePurposes: { researchPurposeTypeCode: { description: any } }[]; + requestSourceType: { description: any }; + requestorPerson: any; + requestDate: any; + researchCompletionDate: any; + }; + const responsePromise = page.waitForResponse( + (response: { url: () => string | string[]; status: () => number }) => + response.url().includes('/api/researchFiles/') && response.status() === 200 + ); + + await test.step('Create file', async () => { + //Navigate to new research file and create a minimum viable research + await researchCreatePage.goto(); + generateFileName('researchFile'); + await researchCreatePage.confirmButtonClick(); + + const response = await responsePromise; + apiFeatureFileJson = await response.json(); + + await expect(researchViewDetails.researchDocumentTab).toBeVisible(); + }); + + await test.step('Validate details', async () => { + const fieldsToCompare = [ + { label: 'Ministry project', apiValue: apiFeatureFileJson.researchFileProjects[0] }, + { label: 'Road name', apiValue: apiFeatureFileJson.roadName }, + { label: 'Road alias', apiValue: apiFeatureFileJson.roadAlias }, + { + label: 'Research purpose', + apiValue: apiFeatureFileJson.researchFilePurposes[0].researchPurposeTypeCode.description, + }, + { label: 'Source of request', apiValue: apiFeatureFileJson.requestSourceType.description }, + { label: 'Requester', apiValue: apiFeatureFileJson.requestorPerson }, + ]; + + const datesToCompare = [ + { label: 'Request date', apiValue: apiFeatureFileJson.requestDate }, + { label: 'Research completed on', apiValue: apiFeatureFileJson.researchCompletionDate }, + ]; + + for (const field of fieldsToCompare) { + const uiValue = await researchViewDetails.getFieldValueByLabel(field.label); + expect(normalize(uiValue)).toBe(normalize(field.apiValue)); + } + + for (const date of datesToCompare) { + const uiValue = await researchViewDetails.getFieldValueByLabel(date.label); + const changedFormat = formatApiDate(date.apiValue); + expect(normalize(uiValue)).toBe(changedFormat); + } + + const isExpropriation = await researchViewDetails.getFieldValueByLabel('Expropriation?'); + const formatedBoolean = formatApiBoolean(apiFeatureFileJson.isExpropriation); + expect(normalize(isExpropriation)).toBe(formatedBoolean); + + const description = await researchViewDetails.getResearchDescription(); + expect(description).toBe(apiFeatureFileJson.requestDescription ?? ''); + + const result = await researchViewDetails.getResearchResult(); + expect(result).toBe(apiFeatureFileJson.researchResult ?? ''); + + const exproNotes = await researchViewDetails.getResearchExpropriationNotes(); + expect(exproNotes).toBe(apiFeatureFileJson.expropriationNotes ?? ''); + }); + + await test.step('validate document upload', async () => { + //Navigate to the Document tab and create a new document + await researchViewDetails.navigateDocumentsTab(); + await documentsListPage.addDocumentButtonClick(); + const documentTypes = await documentUploadModalPage.getSupportedFileExtensionsText(); + expect(documentTypes).toBe( + 'Supported file types include txt, pdf, docx, doc, xlsx, xls, html, odt, png, jpg, bmp, tif, tiff, jpeg, gif, shp, gml, kml, kmz, msg.' + ); + + //Insert an invalid document type + const filePath = path.resolve(process.cwd(), 'fixtures', 'react-icon.svg'); + await documentUploadModalPage.uploadDocument(filePath); + + //Verify warning message appears + const errorLabel = await documentUploadModalPage.getDocumentErrorLabel(); + expect(errorLabel).toBe('File type not supported!'); + + const fileName = await documentUploadModalPage.getDocumentErrorFilename(); + expect(fileName).toBe('react-icon.svg'); + }); + }); +}); diff --git a/testing/pims-integration/utils/utils.ts b/testing/pims-integration/utils/utils.ts new file mode 100644 index 0000000000..dfd5b15c75 --- /dev/null +++ b/testing/pims-integration/utils/utils.ts @@ -0,0 +1,39 @@ +export const generateFileName = (fileType: string): string => { + const now = new Date(); + + const timestamp = + now.getFullYear().toString() + + String(now.getMonth() + 1).padStart(2, '0') + + String(now.getDate()).padStart(2, '0') + + String(now.getHours()).padStart(2, '0') + + String(now.getMinutes()).padStart(2, '0') + + String(now.getSeconds()).padStart(2, '0'); + + return `${fileType}-${timestamp}`; +}; +export const normalize = (value: string | null): string => { + return value == null ? '' : String(value).trim(); +}; + +export const formatApiDate = (apiDate: string | null | undefined): string => { + if (!apiDate) { + return ''; + } + + const date = new Date(apiDate); + + return date.toLocaleDateString('en-CA', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', // avoids timezone surprises + }); +}; + +export const formatApiBoolean = (value: boolean | null | undefined): string => { + if (value == null) { + return ''; + } + + return value ? 'Yes' : 'No'; +};