From d17597949befdc0935f31de5457196576b6b8b53 Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:15:55 +0545 Subject: [PATCH] feat(mapper): frontend login + remove temp auth from React frontend (#1903) * feat(osm-logo): logo add to mapper * feat(login): login store add * fix(layer-switcher): close layer-switcher by default * feat(login): login dialog popup add * fix(login): util functions add for login * fix(login): store current path on localStorage * fix(osmAuth): if user request redirection to mapper frontend then treat as external url * feat(logo): hot logo add * fix(header): header component ui slice * feat(login): toggle modal state add * fix(layout): replace hot-header with custom header component * fix(login): replace modalOpen state on parent with stores * fix(+layout): barlow font add to root * feat(header): display username and profile image if user logged in * fix(login): signOut add * feat(drawerItems): drawer menu items add * feat(header): drawer comp add, signout func add * fix(layerSwitchMenu): width & height fix * fix(mapLegends): fix(mapLegends): show map legend in dropdown * fix(mapControlComponent): add mapLegend to mapControlComponent * fix(projectDetailsV2): remove showing map legend on accordion, btn linking to mapper frontend add * remove(login): remove temporary login from react frontend * test: persist osm login across playwright tests (temp login removed) (#1910) * test: wip persist osm login across playwright tests * test: add temp auth config as a playwright workaround * test: fix mapper flow tests, first open page, then click card * test: disable firefox and webkit tests entirely for now * docs: add note about attempting test auth with webkit * test: use btnTestId prop to identify specific button during tests --------- Co-authored-by: Sam <78538841+spwoodcock@users.noreply.github.com> Co-authored-by: spwoodcock --- src/backend/app/auth/auth_routes.py | 1 - src/frontend/.gitignore | 4 +- .../e2e/01-create-new-project.spec.ts | 8 +- src/frontend/e2e/02-mapper-flow.spec.ts | 21 +-- src/frontend/e2e/auth.setup.ts | 23 ++++ src/frontend/e2e/helpers.ts | 7 +- src/frontend/playwright.config.ts | 32 +++-- src/frontend/src/api/Login.ts | 30 ---- .../src/components/DialogTaskActions.tsx | 1 + src/frontend/src/components/LoginPopup.tsx | 20 +-- .../LayerSwitcher/LayerSwitchMenu.tsx | 2 +- src/frontend/src/components/MapLegends.tsx | 42 +++++- .../ProjectDetailsV2/MapControlComponent.tsx | 6 +- src/frontend/src/components/common/Button.tsx | 4 +- src/frontend/src/routes.jsx | 11 ++ src/frontend/src/views/OsmAuth.tsx | 9 +- .../src/views/PlaywrightTempLogin.tsx | 32 +++++ src/frontend/src/views/ProjectDetailsV2.tsx | 32 ++--- .../src/assets/images/hot-logo-text.svg | 1 + src/mapper/src/assets/images/hot-logo.svg | 1 + src/mapper/src/assets/images/osm-logo.png | Bin 0 -> 8804 bytes src/mapper/src/constants/drawerItems.ts | 27 ++++ src/mapper/src/lib/components/header.svelte | 122 +++++++++++++++++ src/mapper/src/lib/components/login.svelte | 129 ++++++++++++++++++ .../lib/components/map/layer-switcher.svelte | 2 +- src/mapper/src/lib/utils/login.ts | 42 ++++++ src/mapper/src/routes/+layout.svelte | 5 +- src/mapper/src/store/login.svelte.ts | 34 +++++ 28 files changed, 526 insertions(+), 122 deletions(-) create mode 100644 src/frontend/e2e/auth.setup.ts delete mode 100644 src/frontend/src/api/Login.ts create mode 100644 src/frontend/src/views/PlaywrightTempLogin.tsx create mode 100644 src/mapper/src/assets/images/hot-logo-text.svg create mode 100644 src/mapper/src/assets/images/hot-logo.svg create mode 100644 src/mapper/src/assets/images/osm-logo.png create mode 100644 src/mapper/src/constants/drawerItems.ts create mode 100644 src/mapper/src/lib/components/header.svelte create mode 100644 src/mapper/src/lib/components/login.svelte create mode 100644 src/mapper/src/lib/utils/login.ts create mode 100644 src/mapper/src/store/login.svelte.ts diff --git a/src/backend/app/auth/auth_routes.py b/src/backend/app/auth/auth_routes.py index 0482e2b4e4..91df93c4a0 100644 --- a/src/backend/app/auth/auth_routes.py +++ b/src/backend/app/auth/auth_routes.py @@ -315,7 +315,6 @@ async def temp_login( setting it as a cookie. Args: - request (Request): The incoming request object. email: email of non-osm user. Returns: diff --git a/src/frontend/.gitignore b/src/frontend/.gitignore index 4ea574bbeb..2c08d8c2b4 100755 --- a/src/frontend/.gitignore +++ b/src/frontend/.gitignore @@ -122,5 +122,5 @@ dist /playwright-report/ /blob-report/ /playwright/.cache/ - -playwright/.auth +/e2e/.cache/ +e2e/.auth diff --git a/src/frontend/e2e/01-create-new-project.spec.ts b/src/frontend/e2e/01-create-new-project.spec.ts index 2d72ad9e88..184875f308 100644 --- a/src/frontend/e2e/01-create-new-project.spec.ts +++ b/src/frontend/e2e/01-create-new-project.spec.ts @@ -3,18 +3,14 @@ import { test, expect } from '@playwright/test'; -import { tempLogin } from './helpers'; - test('create new project', async ({ browserName, page }) => { // Specific for this large test, only run in one browser // (playwright.config.ts is configured to run all browsers by default) test.skip(browserName !== 'chromium', 'Test only for chromium!'); - // 0. Temp Login - await tempLogin(page); - await page.getByRole('button', { name: '+ Create New Project' }).click(); - // 1. Project Details Step + await page.goto('/'); + await page.getByRole('button', { name: '+ Create New Project' }).click(); await page.getByRole('button', { name: 'NEXT' }).click(); await expect(page.getByText('Project Name is Required.')).toBeVisible(); await expect(page.getByText('Short Description is Required.', { exact: true })).toBeVisible(); diff --git a/src/frontend/e2e/02-mapper-flow.spec.ts b/src/frontend/e2e/02-mapper-flow.spec.ts index 942ff9d347..7ad4450089 100644 --- a/src/frontend/e2e/02-mapper-flow.spec.ts +++ b/src/frontend/e2e/02-mapper-flow.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test'; -import { tempLogin, openTestProject } from './helpers'; +import { openTestProject } from './helpers'; test.describe('mapper flow', () => { test('task actions', async ({ browserName, page }) => { @@ -11,11 +11,8 @@ test.describe('mapper flow', () => { // (playwright.config.ts is configured to run all browsers by default) test.skip(browserName !== 'chromium', 'Test only for chromium!'); - // 0. Temp Login - await tempLogin(page); - await openTestProject(page); - // 1. Click on task area on map + await openTestProject(page); await page.locator('canvas').click({ position: { x: 445, @@ -35,8 +32,8 @@ test.describe('mapper flow', () => { }); // 2. Lock task for mapping - await expect(page.getByRole('button', { name: 'START MAPPING' })).toBeVisible(); - await page.getByRole('button', { name: 'START MAPPING' }).click(); + await expect(page.getByTestId('StartMapping')).toBeVisible(); + await page.getByTestId('StartMapping').click(); await page.waitForSelector('div:has-text("updated to LOCKED_FOR_MAPPING"):nth-of-type(1)'); await expect( page @@ -107,12 +104,9 @@ test.describe('mapper flow', () => { // (playwright.config.ts is configured to run all browsers by default) test.skip(browserName !== 'chromium', 'Test only for chromium!'); - // 0. Temp Login - await tempLogin(page); - await openTestProject(page); - // 1. Click on task area on map // click on task & assert task popup visibility + await openTestProject(page); await page.locator('canvas').click({ position: { x: 388, @@ -120,7 +114,7 @@ test.describe('mapper flow', () => { }, }); await expect(page.getByText('Status: UNLOCKED_TO_MAP')).toBeVisible(); - await expect(page.getByRole('button', { name: 'START MAPPING' })).toBeVisible(); + await expect(page.getByTestId('StartMapping')).toBeVisible(); // 2. Click on a specific feature / Entity within a task // assert feature popup visibility @@ -176,10 +170,7 @@ test.describe('mapper flow', () => { // (playwright.config.ts is configured to run all browsers by default) test.skip(browserName !== 'chromium', 'Test only for chromium!'); - // 0. Temp Login - await tempLogin(page); await openTestProject(page); - await page.locator('canvas').click({ position: { x: 475, diff --git a/src/frontend/e2e/auth.setup.ts b/src/frontend/e2e/auth.setup.ts new file mode 100644 index 0000000000..da517e29a6 --- /dev/null +++ b/src/frontend/e2e/auth.setup.ts @@ -0,0 +1,23 @@ +import { test as setup } from '@playwright/test'; +import path from 'path'; + +const authFile = path.join(__dirname, './.auth/user.json'); + +setup('authenticate', async ({ browserName, page }) => { + // Note here we only run in chromium, to avoid running this setup step + // for Firefox and Webkit. + // This is because Webkit does not respect 'secure' cookies on http contexts. + // For this to work we would need to configure https for testing + // https://github.com/hotosm/fmtm/pull/1920 + setup.skip(browserName !== 'chromium', 'Test only for chromium!'); + + // Note this sets a token so we can proceed, but the login will be + // overwritten by svcfmtm localadmin user (as DEBUG=True) + await page.goto('/playwright-temp-login/'); + + // Now check we are signed in as localadmin + await page.waitForSelector('text=localadmin'); + + // Save authentication state + await page.context().storageState({ path: authFile }); +}); diff --git a/src/frontend/e2e/helpers.ts b/src/frontend/e2e/helpers.ts index 5c5a3d26da..7c11160181 100644 --- a/src/frontend/e2e/helpers.ts +++ b/src/frontend/e2e/helpers.ts @@ -1,12 +1,7 @@ import { Page } from '@playwright/test'; -export async function tempLogin(page: Page) { - await page.goto('/'); - await page.getByRole('button', { name: 'Sign in' }).click(); - await page.getByText('Temporary Account').click(); -} - export async function openTestProject(page: Page) { + await page.goto('/'); // open project card with regex text 'Project Create Playwright xxx' await page .getByText(/^Project Create Playwright/) diff --git a/src/frontend/playwright.config.ts b/src/frontend/playwright.config.ts index dfa5cf7cbf..0ae733f208 100644 --- a/src/frontend/playwright.config.ts +++ b/src/frontend/playwright.config.ts @@ -28,18 +28,32 @@ export default defineConfig({ /* Configure projects for major browsers */ projects: [ + // Setup project + { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', - use: { browserName: 'chromium' }, - }, - { - name: 'firefox', - use: { browserName: 'firefox' }, - }, - { - name: 'webkit', - use: { browserName: 'webkit' }, + use: { + browserName: 'chromium', + storageState: 'e2e/.auth/user.json', + }, + dependencies: ['setup'], }, + // { + // name: 'firefox', + // use: { + // browserName: 'firefox', + // storageState: 'e2e/.auth/user.json', + // }, + // dependencies: ['setup'], + // }, + // { + // name: 'webkit', + // use: { + // browserName: 'webkit', + // storageState: 'playwright/.auth/user.json', + // }, + // dependencies: ['setup'], + // }, /* Test against mobile viewports. */ // { diff --git a/src/frontend/src/api/Login.ts b/src/frontend/src/api/Login.ts deleted file mode 100644 index e954220ffb..0000000000 --- a/src/frontend/src/api/Login.ts +++ /dev/null @@ -1,30 +0,0 @@ -import axios from 'axios'; -import { getUserDetailsFromApi } from '@/utilfunctions/login'; -import { CommonActions } from '@/store/slices/CommonSlice'; -import { LoginActions } from '@/store/slices/LoginSlice'; - -export const TemporaryLoginService: Function = (url: string) => { - return async (dispatch) => { - const getTemporaryLogin = async (url: string) => { - // Sets a cookie in the browser that is used for auth - await axios.get(url); - - const apiUser = await getUserDetailsFromApi(); - if (!apiUser) { - dispatch( - CommonActions.SetSnackBar({ - open: true, - message: 'Temp login failed. Try OSM.', - variant: 'error', - duration: 2000, - }), - ); - return; - } - - dispatch(LoginActions.setAuthDetails(apiUser)); - }; - - await getTemporaryLogin(url); - }; -}; diff --git a/src/frontend/src/components/DialogTaskActions.tsx b/src/frontend/src/components/DialogTaskActions.tsx index e44b203134..a81d211feb 100755 --- a/src/frontend/src/components/DialogTaskActions.tsx +++ b/src/frontend/src/components/DialogTaskActions.tsx @@ -184,6 +184,7 @@ export default function Dialog({ taskId, feature }: dialogPropType) { return list_of_task_actions?.length != 0 ? (