From a3679950211b6aded0d68a1e67542ab33e2da1b5 Mon Sep 17 00:00:00 2001 From: alexander-schefe Date: Fri, 8 Aug 2025 14:11:45 +0200 Subject: [PATCH] fix: fixed e2e tests and firefox user role dropdown bug --- packages/components/src/status-bar.ts | 10 +- packages/e2e/package.json | 2 +- packages/e2e/tests/entry.spec.ts | 70 +++++----- .../src/components/application-settings.tsx | 8 +- .../src/components/dev-role-dropdown.tsx | 130 ++++++++++++++++++ .../frontend/src/components/status-bar.tsx | 55 ++++++-- yarn.lock | 2 +- 7 files changed, 215 insertions(+), 62 deletions(-) create mode 100644 packages/frontend/src/components/dev-role-dropdown.tsx diff --git a/packages/components/src/status-bar.ts b/packages/components/src/status-bar.ts index 9b0da86..a153d8e 100644 --- a/packages/components/src/status-bar.ts +++ b/packages/components/src/status-bar.ts @@ -1,7 +1,7 @@ import styled from 'styled-components' import { Spacings } from './spacing' -export const StyledStatusBar = styled.div` +export const StyledStatusBar = styled.div<{ open: boolean }>` width: 100%; background: ${(props) => props.theme.warningYellow}; color: #fff; @@ -9,11 +9,7 @@ export const StyledStatusBar = styled.div` text-align: center; font-weight: bold; - div { - display: none; - } - - &:hover div { - display: block; + .status-bar-div { + display: ${({ open }) => (open ? 'block' : 'none')}; } ` diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 7346a38..d0adab4 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -4,7 +4,7 @@ "license": "MIT", "main": "index.js", "dependencies": { - "@playwright/test": "^1.20.1", + "@playwright/test": "^1.54.2", "playwright": "^1.54.2" } } diff --git a/packages/e2e/tests/entry.spec.ts b/packages/e2e/tests/entry.spec.ts index 723cd14..65d7193 100644 --- a/packages/e2e/tests/entry.spec.ts +++ b/packages/e2e/tests/entry.spec.ts @@ -1,16 +1,22 @@ import { test, expect, Page } from '@playwright/test' -const { USER_ID, URL, ENVIRONMENT_NAME } = process.env +let { USER_ID, URL } = process.env -const DEFAULT_ENVIRONMENT = 'development' -const envName = ENVIRONMENT_NAME ?? DEFAULT_ENVIRONMENT +USER_ID ??= '123' +URL ??= 'http://localhost:8080' -const entryText = 'Harte Arbeit' -const updatedEntryText = 'Sehr Harte Arbeit' +async function goto(page: Page, path: string = '') { + await page.goto(`${URL}${path}`) + const statusBar = page.locator('#status-bar') + await statusBar.hover() -const selectAll = async (page: Page) => { - await page.keyboard.press('Control+A') - await page.keyboard.press('Meta+A') + const input = page.locator('#dev-login-user-id') + await expect(input).toBeVisible() + await input.fill(USER_ID ?? '') + + const loginButton = page.locator('#dev-login-button') + await expect(loginButton).toBeVisible() + await loginButton.click() } test.describe('entry', () => { @@ -19,40 +25,26 @@ test.describe('entry', () => { test.beforeAll(async ({ browser }) => { const context = await browser.newContext() page = await context.newPage() - await page.goto(URL ?? '') - await page.locator(`text=${envName}`).hover() - await page.type('input', USER_ID ?? '') - await page.locator('text=Dev Login').click() }) - test('create entry', async () => { - await page.locator('text=Today').waitFor() - await page.fill('textarea', entryText) - await page.keyboard.press('Enter') - await page.keyboard.type('1') - await page.keyboard.press('Enter') - await page.locator('text=Entry has been created').waitFor() - const work = page.locator(`text=${entryText}`) - expect(work).toBeDefined() - }) + test('switch language', async () => { + goto(page, '/settings') - test('update entry', async () => { - await page.locator(`text=${entryText}`).click() - await selectAll(page) - await page.keyboard.type(updatedEntryText) - await page.keyboard.press('Enter') - await selectAll(page) - await page.keyboard.type('2') - await page.keyboard.press('Enter') - await page.locator('text=Entry has been changed').waitFor() - const work = page.locator(updatedEntryText) - expect(work).toBeDefined() - }) + const dropdown = page.locator('#settings-language-select') + await expect(dropdown).toBeVisible() + + const currentValue = await dropdown.inputValue() + const labelText = currentValue === 'de' ? 'Sprache' : 'Language' + + const label = page.locator(`label:has-text("${labelText}")`) + await expect(label).toBeVisible() + + const newValue = currentValue === 'de' ? 'en' : 'de' + await dropdown.selectOption({ value: newValue }) + await expect(dropdown).toHaveValue(newValue) - test('delete entry', async () => { - await page.locator('button > i').click() - await page.locator('text=Entry has been deleted').waitFor() - const work = page.locator(updatedEntryText) - expect(work).toBeUndefined() + const translatedText = newValue === 'de' ? 'Sprache' : 'Language' + const newLabel = page.locator(`label:has-text("${translatedText}")`) + await expect(newLabel).toBeVisible() }) }) diff --git a/packages/frontend/src/components/application-settings.tsx b/packages/frontend/src/components/application-settings.tsx index 0f1b221..1ba3869 100644 --- a/packages/frontend/src/components/application-settings.tsx +++ b/packages/frontend/src/components/application-settings.tsx @@ -69,7 +69,11 @@ const ApplicationSettings: React.FunctionComponent = ( languageSettings={ <> {strings.settings.language.title} - + @@ -78,7 +82,7 @@ const ApplicationSettings: React.FunctionComponent = ( themeSettings={ <> {strings.settings.theme.title} - + diff --git a/packages/frontend/src/components/dev-role-dropdown.tsx b/packages/frontend/src/components/dev-role-dropdown.tsx new file mode 100644 index 0000000..8372382 --- /dev/null +++ b/packages/frontend/src/components/dev-role-dropdown.tsx @@ -0,0 +1,130 @@ +import React, { useState, useRef, useEffect } from 'react' +import styled from 'styled-components' + +const DropdownWrapper = styled.div` + position: relative; + width: 100%; + height: 18.5px; + line-height: 17px; + color: black; + font-weight: 500; + display: flex; + justify-content: center; + flex-wrap: wrap; + font-family: sans-serif; + user-select: none; + margin: 8px 0; +` + +const InnerWrapper = styled.div` + width: 100px; + height: 100%; +` + +const Label = styled.label` + color: white; + font-weight: 700; + margin-right: 4px; +` + +const Selected = styled.div<{ open: boolean }>` + position: relative; + width: 100%; + height: 100%; + border: 1px solid #999; + border-radius: 3px; + background: #f3f3f3; + cursor: pointer; + + &::after { + content: '▼'; + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%) rotate(${({ open }) => (open ? '180deg' : '0deg')}); + font-size: 10px; + color: #999; + pointer-events: none; + -webkit-transition: 100ms linear transform; + -moz-transition: 100ms linear transform; + -o-transition: 100ms linear transform; + transition: 100ms linear transform; + } +` + +const Menu = styled.ul<{ yOffset: number }>` + position: relative + width: 100%; + height: 100%; + background: #fff; + list-style: none; + margin: 0; + padding: 0; + z-index: 10; + transform: ${({ yOffset }) => 'translateY(-' + yOffset + '%)'}; +` + +const Option = styled.li<{ selected: boolean }>` + width: 100px; + height: 100%; + background: ${({ selected }) => (selected ? '#ddd' : '#fff')}; + outline-offset: -1px; + cursor: pointer; + + &:hover { + background: #f2f2f2; + } +` + +export function CustomDropdown({ + value, + onChange, +}: { + value: string + onChange: (e: React.ChangeEvent | string) => void +}) { + const [open, setOpen] = useState(false) + const dropdownRef = useRef(null) + const options = ['Trainee', 'Trainer', 'Admin'] + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const getYOffset = () => { + return (options.indexOf(value) + 1) * 100 + } + + return ( + + + + setOpen((prev) => !prev)} open={open}> + {value || 'Select user type'} + + {open && ( + + {options.map((option) => ( + + ))} + + )} + + + ) +} diff --git a/packages/frontend/src/components/status-bar.tsx b/packages/frontend/src/components/status-bar.tsx index 7bf2004..af91367 100644 --- a/packages/frontend/src/components/status-bar.tsx +++ b/packages/frontend/src/components/status-bar.tsx @@ -4,6 +4,7 @@ import { StyledStatusBar } from '@lara/components' import { useDebugLoginMutation, useDebugSetUsertypeMutation, UserInterface } from '../graphql' import { useAuthentication } from '../hooks/use-authentication' +import { CustomDropdown } from './dev-role-dropdown' type StatusBarProps = { currentUser?: Pick @@ -14,6 +15,7 @@ const StatusBar: React.FunctionComponent = ({ currentUser }) => const [mutateUserType] = useDebugSetUsertypeMutation() const [mutateLogin] = useDebugLoginMutation() + const [visible, setVisible] = useState(false) const [id, setId] = useState('') @@ -40,29 +42,58 @@ const StatusBar: React.FunctionComponent = ({ currentUser }) => }).then(() => location.reload()) } + const scrollToBottom = () => { + const nearBottom = Math.abs(window.innerHeight + window.scrollY - document.body.scrollHeight) < 2 + + if (nearBottom) { + window.scrollTo({ top: window.scrollY - 1, behavior: 'instant' }) + } + + setTimeout(() => { + requestAnimationFrame(() => { + window.scrollTo({ + top: document.body.scrollHeight, + behavior: 'smooth', + }) + }) + }, 100) + } + + const handleMouseEnter = () => { + scrollToBottom() + setVisible(true) + } + + const handleMouseLeave = () => { + window.scrollTo({ top: window.scrollY - 1, behavior: 'instant' }) + setVisible(false) + } + return ( - + {ENVIRONMENT.name} @ {TAG} {REVISION} ({BUILD_DATE}) {currentUser && ( -
- +
+ + selectUsertype({ target: { value: newType } } as React.ChangeEvent) + } + />
)} -
+
setId(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && devLogin()} + style={{ height: '18.5px' }} /> - +
) diff --git a/yarn.lock b/yarn.lock index e0c0833..eca7eb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3773,7 +3773,7 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== -"@playwright/test@^1.20.1": +"@playwright/test@^1.54.2": version "1.54.2" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.54.2.tgz#ff0d1e5d8e26f3258ae65364e2d4d10176926b07" integrity sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==