diff --git a/Makefile b/Makefile index 9b621218cf..21aea4eb24 100644 --- a/Makefile +++ b/Makefile @@ -40,9 +40,9 @@ stop-stubbed-apis: clean: docker-env rm -Rf ./backend/target docker compose down -v - docker compose --env-file ./infra/docker/.env -f ./infra/docker/docker-compose.monitorenv.dev.yml down -v docker compose --env-file ./infra/docker/.env -f ./infra/docker/docker-compose.cypress.yml down -v docker compose -f ./infra/docker/docker-compose.puppeteer.yml down -v + docker compose --env-file ./infra/docker/.env -f ./infra/docker/docker-compose.monitorenv.dev.yml down -v check-clean-archi: cd backend/tools && ./check-clean-architecture.sh diff --git a/frontend/puppeteer/e2e/missions.spec.ts b/frontend/puppeteer/e2e/mission_form_synchronization.spec.ts similarity index 95% rename from frontend/puppeteer/e2e/missions.spec.ts rename to frontend/puppeteer/e2e/mission_form_synchronization.spec.ts index f8cc85ac6a..9e873d7c00 100644 --- a/frontend/puppeteer/e2e/missions.spec.ts +++ b/frontend/puppeteer/e2e/mission_form_synchronization.spec.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from '@jest/globals' import { platform } from 'os' import { Page } from 'puppeteer' -import { assertContains, getFirstTab, getInputContent, listenToConsole, wait, waitForSelectorWithText } from './utils' +import { assertContains, getPage, getInputContent, listenToConsole, wait, waitForSelectorWithText } from './utils' import { SeaFrontGroup } from '../../src/domain/entities/seaFront/constants' const TIMEOUT = 120 * 1000 @@ -17,12 +17,12 @@ const URL = `http://${WEBAPP_HOST}:${WEBAPP_PORT}/side_window` let pageA: Page let pageB: Page -describe('Missions Form', () => { +describe('Missions form synchronization', () => { beforeEach(async () => { - pageA = await getFirstTab(browsers[0]) + pageA = await getPage(browsers[0]) listenToConsole(pageA, 1) - pageB = await getFirstTab(browsers[1]) + pageB = await getPage(browsers[1]) listenToConsole(pageB, 2) /* eslint-disable no-restricted-syntax */ @@ -165,7 +165,10 @@ describe('Missions Form', () => { */ const finalReopen = await pageA.waitForSelector('[data-cy="reopen-mission"]') await finalReopen.click() + await wait(5000) + await pageA.close() + await pageB.close() }, TIMEOUT ) diff --git a/frontend/puppeteer/e2e/mission_side_window.spec.ts b/frontend/puppeteer/e2e/mission_side_window.spec.ts new file mode 100644 index 0000000000..ac5946aae5 --- /dev/null +++ b/frontend/puppeteer/e2e/mission_side_window.spec.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it } from '@jest/globals' +import { platform } from 'os' +import { ElementHandle, Page } from 'puppeteer' + +import { assertContains, getPage, getInputContent, getSideWindow, listenToConsole, wait } from './utils' + +const TIMEOUT = 120 * 1000 + +const IS_CI = Boolean(process.env.CI) +const IS_DARWIN = platform() === 'darwin' +const WEBAPP_PORT = IS_CI ? 8880 : 3000 +const WEBAPP_HOST = IS_DARWIN ? '0.0.0.0' : 'localhost' + +const URL = `http://${WEBAPP_HOST}:${WEBAPP_PORT}/` + +let mainWindow: Page + +describe('Side window', () => { + beforeEach(async () => { + mainWindow = await getPage(browsers[0]) + listenToConsole(mainWindow, 1) + + await mainWindow.goto(URL, { waitUntil: 'domcontentloaded' }) + await wait(2000) + }, 50000) + + it( + 'A control must replace another control previously opened in the side window', + async () => { + /** + * Open vessel sidebar Controls tab + */ + await wait(2000) + const searchVessel = await mainWindow.waitForSelector('input[placeholder="Rechercher un navire..."]') + searchVessel.click() + mainWindow.type('input[placeholder="Rechercher un navire..."]', 'pheno', { delay: 50 }) + + const foundVessel = await mainWindow.waitForSelector('mark') + foundVessel.click() + + await wait(2000) + const controlsTab = await mainWindow.waitForSelector('*[data-cy="vessel-menu-controls"]') + controlsTab.click() + + await wait(1000) + await mainWindow.waitForSelector('*[data-cy="vessel-controls-year"]') + const years = await mainWindow.$$('*[data-cy="vessel-controls-year"]') + console.log(`Found ${years.length} year`) + years.forEach(async year => { + year.click() + await wait(200) + }) + await wait(1000) + + /** + * Open first control in side window + */ + await mainWindow.waitForXPath("//span[contains(text(), 'Ouvrir le contrôle')]/..") + const openControlButtons = (await mainWindow.$x( + "//span[contains(text(), 'Ouvrir le contrôle')]/.." + )) as ElementHandle[] + console.log(`Found ${openControlButtons.length} controls`) + + const firstControlButton = openControlButtons[0] + if (!firstControlButton) { + throw new Error('The first control button is undefined') + } + await firstControlButton.click() + + await wait(2000) + const sideWindow = await getSideWindow() + if (!sideWindow) { + throw new Error('sideWindow page is undefined') + } + + await assertContains(sideWindow, '.Element-Tag', 'Clôturée') + await assertContains(sideWindow, '.Element-Tag', 'Appréhension espèce') + await wait(1000) + + /** + * Open another control in side window + */ + await mainWindow.focus('body') + await mainWindow.waitForXPath("//span[contains(text(), 'Ouvrir le contrôle')]/..") + const openSecondControlButtons = (await mainWindow.$x( + "//span[contains(text(), 'Ouvrir le contrôle')]/.." + )) as ElementHandle[] + const secondControlButton = openSecondControlButtons[1] + if (!secondControlButton) { + throw new Error('The second control button is undefined') + } + await secondControlButton.click() + + /** + * Modify contact on second control + */ + const controlUnitContact = await sideWindow.waitForSelector('[name="contact_0"]') + await controlUnitContact.click({ clickCount: 3, delay: 50 }) + await controlUnitContact.type('A new tel. number', { delay: 50 }) + await wait(1000) + + /** + * Open first control again + */ + await firstControlButton.click() + + /** + * The control and mission form should be unmodified + */ + await assertContains(sideWindow, '.Element-Tag', 'Appréhension espèce') + expect(await getInputContent(sideWindow, '[name="contact_0"]')).toBe('') + + await wait(5000) + await sideWindow.close() + await mainWindow.close() + }, + TIMEOUT + ) +}) diff --git a/frontend/puppeteer/e2e/utils.ts b/frontend/puppeteer/e2e/utils.ts index 7c1465e449..3c2841ed64 100644 --- a/frontend/puppeteer/e2e/utils.ts +++ b/frontend/puppeteer/e2e/utils.ts @@ -5,7 +5,7 @@ export function listenToConsole(page: Page, index: number) { page .on('console', message => { const messageType = message.type().substr(0, 3).toUpperCase() - console.log(`[Page ${index}] ${messageType}: ${message.text()}`) + console.log(`[Page ${index}] ${messageType}: ${JSON.stringify(message.text())}`) if (messageType === 'ERR') { console.log(message.args(), message.stackTrace()) @@ -14,6 +14,11 @@ export function listenToConsole(page: Page, index: number) { return } + if (message.text().includes('/wfs') || message.text().includes('areas')) { + // Do not throw an error when the app could not load a layer + return + } + throw new Error(message.text()) } }) @@ -49,10 +54,10 @@ export async function getInputContent(page: Page, selector: string) { return element && element.evaluate((el: HTMLInputElement) => el.value) } -export async function getFirstTab(browser: Browser) { - const [firstTab] = await browser.pages() +export async function getPage(browser: Browser) { + const page = await browser.newPage() - return firstTab as Page + return page as Page } export function wait(ms: number) { @@ -68,3 +73,9 @@ export async function waitForSelectorWithText( ) { await page.waitForFunction(`document.querySelector("${selector}").innerText.includes("${text}")`, options) } + +export async function getSideWindow() { + const lastTarget = browsers[0].targets().length - 1 + + return browsers[0].targets()[lastTarget]?.page() +} diff --git a/frontend/src/features/Mission/components/MissionForm/MainForm/FormikMultiControlUnitPicker/ControlUnitSelect.tsx b/frontend/src/features/Mission/components/MissionForm/MainForm/FormikMultiControlUnitPicker/ControlUnitSelect.tsx index ad3509f4f7..353009edaa 100644 --- a/frontend/src/features/Mission/components/MissionForm/MainForm/FormikMultiControlUnitPicker/ControlUnitSelect.tsx +++ b/frontend/src/features/Mission/components/MissionForm/MainForm/FormikMultiControlUnitPicker/ControlUnitSelect.tsx @@ -39,6 +39,7 @@ type ControlUnitSelectProps = Readonly<{ ) => Promisable onDelete: (index: number) => Promisable }> + export function ControlUnitSelect({ allAdministrationsAsOptions, allControlUnits, @@ -61,7 +62,7 @@ export function ControlUnitSelect({ [allControlUnits, value] ) - const isLoading = !allControlUnits.length + const isLoading = !activeAndSelectedControlUnits.length const isEdition = selectedPath.id const { data: engagedControlUnits = [] } = useGetEngagedControlUnitsQuery(undefined, { @@ -115,7 +116,8 @@ export function ControlUnitSelect({ const handleNameChange = useCallback( (nextName: string | undefined) => { - if (isLoading) { + const isSameValue = nextName === value.name + if (isLoading || isSameValue) { return } @@ -125,7 +127,7 @@ export function ControlUnitSelect({ ? { ...nextSelectedControlUnit, contact: value.contact, - resources: value.resources + resources: [] } : { ...INITIAL_MISSION_CONTROL_UNIT, @@ -151,6 +153,10 @@ export function ControlUnitSelect({ const handleResourcesChange = useCallback( (nextResources: LegacyControlUnit.LegacyControlUnitResource[] | undefined) => { + if (isLoading) { + return + } + const nextControlUnit: LegacyControlUnit.LegacyControlUnitDraft = { ...value, resources: nextResources ?? [] @@ -158,7 +164,7 @@ export function ControlUnitSelect({ onChange(index, nextControlUnit) }, - [value, index, onChange] + [value, index, onChange, isLoading] ) const handleContactChange = useCallback( diff --git a/frontend/src/features/SideWindow/SideWindowLauncher.tsx b/frontend/src/features/SideWindow/SideWindowLauncher.tsx index 610953fca5..d4c1ca7642 100644 --- a/frontend/src/features/SideWindow/SideWindowLauncher.tsx +++ b/frontend/src/features/SideWindow/SideWindowLauncher.tsx @@ -45,7 +45,7 @@ export function SideWindowLauncher() { showPrompt={isDraftDirty} title="MonitorFish" > - + ) } diff --git a/frontend/src/features/SideWindow/index.tsx b/frontend/src/features/SideWindow/index.tsx index 602fdd663a..5d6a0ae821 100644 --- a/frontend/src/features/SideWindow/index.tsx +++ b/frontend/src/features/SideWindow/index.tsx @@ -36,9 +36,9 @@ import { setEditedReportingInSideWindow } from '../Reporting/slice' import { getAllCurrentReportings } from '../Reporting/useCases/getAllCurrentReportings' export type SideWindowProps = HTMLAttributes & { - isFromURL: boolean + isFromURL?: boolean } -export function SideWindow({ isFromURL }: SideWindowProps) { +export function SideWindow({ isFromURL = false }: SideWindowProps) { const dispatch = useMainAppDispatch() // eslint-disable-next-line no-null/no-null const wrapperRef = useRef(null) diff --git a/infra/docker/docker-compose.puppeteer.yml b/infra/docker/docker-compose.puppeteer.yml index 7387f0d3cd..79292d1b2a 100644 --- a/infra/docker/docker-compose.puppeteer.yml +++ b/infra/docker/docker-compose.puppeteer.yml @@ -116,7 +116,7 @@ services: ports: - 8081:8080 volumes: - - ./mappings:/home/wiremock/mappings + - ../../frontend/cypress/mappings:/home/wiremock/mappings healthcheck: test: [ "CMD-SHELL",