diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2c5638a..78f0b099 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,32 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v5 + - name: Setup Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.90.0 + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Build overlay wasm package + working-directory: ./crates/wasm/overlay + run: wasm-pack build --dev --target web --out-dir ../../../ui/webapp/wasm/overlay --target-dir ../../../target-wasm + - name: Build quiz wasm package + working-directory: ./crates/wasm/quiz + run: wasm-pack build --dev --target web --out-dir ../../../ui/webapp/wasm/quiz --target-dir ../../../target-wasm + - name: Install shared package dependencies + working-directory: ./ui/common + run: yarn install --network-concurrency 1 + - name: Build shared package + working-directory: ./ui/common + run: yarn build - name: Install dependencies working-directory: ./ui/webapp run: yarn install --network-concurrency 1 + - name: Install Playwright browsers + working-directory: ./ui/webapp + run: npx playwright install --with-deps + env: + PLAYWRIGHT_BROWSERS_PATH: 0 - name: Run prettier working-directory: ./ui/webapp run: yarn format:diff @@ -47,6 +70,11 @@ jobs: - name: Run tests working-directory: ./ui/webapp run: yarn test + - name: Run e2e tests + working-directory: ./ui/webapp + run: yarn test:e2e + env: + PLAYWRIGHT_BROWSERS_PATH: 0 lint-and-test-embed: runs-on: diff --git a/.gitignore b/.gitignore index 83104dce..2cb37ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,9 @@ ui/embed-item/node_modules ui/webapp/dist ui/webapp/node_modules ui/webapp/static/* +ui/webapp/playwright-report +ui/webapp/test-results +ui/webapp/blob-report +ui/webapp/coverage yarn-debug.log* yarn-error.log* diff --git a/ui/webapp/package.json b/ui/webapp/package.json index 57749fb6..c6de3626 100644 --- a/ui/webapp/package.json +++ b/ui/webapp/package.json @@ -13,7 +13,9 @@ "preview": "vite preview", "test": "jest", "test:watch": "jest --watch", - "test:coverage": "jest --coverage" + "test:coverage": "jest --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@solid-primitives/resize-observer": "^2.1.3", @@ -31,6 +33,7 @@ "yarn": "^1.22.22" }, "devDependencies": { + "@playwright/test": "^1.56.1", "@types/jest": "^30.0.0", "@types/lodash": "^4.17.20", "eslint": "^9.36.0", diff --git a/ui/webapp/playwright.config.ts b/ui/webapp/playwright.config.ts new file mode 100644 index 00000000..e512b2df --- /dev/null +++ b/ui/webapp/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:5173', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure' + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } } + ], + webServer: { + command: 'yarn dev -- --host 127.0.0.1 --port 5173', + url: 'http://127.0.0.1:5173', + reuseExistingServer: !process.env.CI, + timeout: 120000, + env: { + SASS_SILENCE_DEPRECATION_WARNINGS: '1' + } + } +}); diff --git a/ui/webapp/src/layout/games/Content.tsx b/ui/webapp/src/layout/games/Content.tsx index 457d8e05..dedb4a58 100644 --- a/ui/webapp/src/layout/games/Content.tsx +++ b/ui/webapp/src/layout/games/Content.tsx @@ -4,12 +4,39 @@ import isUndefined from 'lodash/isUndefined'; import { createSignal, For, onMount, Show } from 'solid-js'; import init, { Quiz, QuizOptions, State } from '../../../wasm/quiz/landscape2_quiz'; +import { BASE_PATH } from '../../data'; import pattern from '../../media/pattern_quiz.png'; import isWasmSupported from '../../utils/isWasmSupported'; import itemsDataGetter from '../../utils/itemsDataGetter'; import styles from './Content.module.css'; import Title from './Title'; +const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, ''); + +const buildPath = (...segments: (string | undefined)[]) => { + const parts = segments + .map((segment) => segment?.trim()) + .filter((segment) => !isUndefined(segment) && segment !== '') + .map((segment) => trimSlashes(segment!)) + .filter((segment) => segment.length > 0); + + if (parts.length === 0) { + return ''; + } + + return `/${parts.join('/')}`; +}; + +const normalizedBasePath = buildPath(BASE_PATH); +const devStaticPath = buildPath(BASE_PATH, 'static') || '/static'; + +const getQuizBaseUrl = () => { + if (import.meta.env.MODE === 'development') { + return `${window.location.origin}${devStaticPath}`; + } + return `${window.location.origin}${normalizedBasePath}`; +}; + const Content = () => { const [loaded, setLoaded] = createSignal(false); const [activeQuiz, setActiveQuiz] = createSignal(null); @@ -18,7 +45,7 @@ const Content = () => { const [error, setError] = createSignal(); const startGame = async (initiated?: boolean) => { - const options = new QuizOptions(import.meta.env.MODE === 'development' ? 'http://localhost:8000' : location.origin); + const options = new QuizOptions(getQuizBaseUrl()); const quiz = await Quiz.new(options); setActiveQuiz(quiz); if (initiated) setQuizState(quiz.state()); @@ -79,7 +106,7 @@ const Content = () => {
{quizState()!.score.wrong}
diff --git a/ui/webapp/src/layout/games/Title.tsx b/ui/webapp/src/layout/games/Title.tsx index b16fcdcc..9aaa81bd 100644 --- a/ui/webapp/src/layout/games/Title.tsx +++ b/ui/webapp/src/layout/games/Title.tsx @@ -43,7 +43,7 @@ const Title = (props: Props) => { ); return ( -
+
{ createEffect( on(displayItemModal, () => { if (displayItemModal()) { - import(`${embedOrigin()}/embed-item.js`).then(() => { + import(/* @vite-ignore */ `${embedOrigin()}/embed-item.js`).then(() => { setEmbedScriptLoaded(true); }); } diff --git a/ui/webapp/tests/e2e/data/base.json b/ui/webapp/tests/e2e/data/base.json new file mode 100644 index 00000000..3ca92c6d --- /dev/null +++ b/ui/webapp/tests/e2e/data/base.json @@ -0,0 +1,192 @@ +{ + "finances_available": false, + "foundation": "DEMO", + "description": "Demo landscape for local development.", + "url": "http://127.0.0.1:8000", + "colors": { + "color1": "rgba(0, 107, 204, 1)", + "color2": "rgba(255, 0, 170, 1)", + "color3": "rgba(96, 149, 214, 1)", + "color4": "rgba(0, 42, 81, 0.7)", + "color5": "rgba(1, 107, 204, 0.7)", + "color6": "rgba(0, 42, 81, 0.7)", + "color7": "rgba(180, 219, 255, 1)" + }, + "grid_items_size": "large", + "games_available": [ + "quiz" + ], + "guide_summary": { + "Category 1": [ + "Summary", + "Subcategory 1-1", + "Subcategory 1-2" + ], + "Category 2": [ + "Summary", + "Subcategory 2-1", + "Subcategory 2-2" + ] + }, + "groups": [ + { + "name": "Some categories", + "normalized_name": "some-categories", + "categories": [ + "Category 1", + "Category 2" + ] + }, + { + "name": "Only category 2", + "normalized_name": "only-category-2", + "categories": [ + "Category 2" + ] + } + ], + "categories": [ + { + "name": "Category 1", + "normalized_name": "category-1", + "subcategories": [ + { + "name": "Subcategory 1-1", + "normalized_name": "subcategory-1-1" + }, + { + "name": "Subcategory 1-2", + "normalized_name": "subcategory-1-2" + } + ] + }, + { + "name": "Category 2", + "normalized_name": "category-2", + "subcategories": [ + { + "name": "Subcategory 2-1", + "normalized_name": "subcategory-2-1" + }, + { + "name": "Subcategory 2-2", + "normalized_name": "subcategory-2-2" + } + ] + } + ], + "items": [ + { + "id": "category-1--subcategory-1-1--item-1", + "category": "Category 1", + "subcategory": "Subcategory 1-1", + "name": "Item 1", + "logo": "logos/cncf.svg", + "description": "This is the description of item 1", + "maturity": "graduated" + }, + { + "id": "category-1--subcategory-1-1--item-2", + "category": "Category 1", + "subcategory": "Subcategory 1-1", + "name": "Item 2", + "logo": "logos/cncf.svg", + "description": "This is the description of item 2", + "maturity": "sandbox" + }, + { + "id": "category-1--subcategory-1-1--item-3", + "category": "Category 1", + "subcategory": "Subcategory 1-1", + "name": "Item 3", + "logo": "logos/cncf.svg", + "description": "This is the description of item 3" + }, + { + "id": "category-1--subcategory-1-2--item-4", + "category": "Category 1", + "subcategory": "Subcategory 1-2", + "name": "Item 4", + "logo": "logos/cncf.svg", + "description": "This is the description of item 4" + }, + { + "id": "category-1--subcategory-1-2--item-5", + "category": "Category 1", + "subcategory": "Subcategory 1-2", + "name": "Item 5", + "logo": "logos/cncf.svg", + "description": "This is the description of item 5" + }, + { + "id": "category-2--subcategory-2-1--item-6", + "category": "Category 2", + "subcategory": "Subcategory 2-1", + "name": "Item 6", + "logo": "logos/cncf.svg", + "description": "This is the description of item 6", + "maturity": "graduated" + }, + { + "id": "category-2--subcategory-2-1--item-7", + "category": "Category 2", + "subcategory": "Subcategory 2-1", + "name": "Item 7", + "logo": "logos/cncf.svg", + "description": "This is the description of item 7", + "maturity": "sandbox" + }, + { + "id": "category-2--subcategory-2-1--item-8", + "category": "Category 2", + "subcategory": "Subcategory 2-1", + "name": "Item 8", + "logo": "logos/cncf.svg", + "description": "This is the description of item 8" + }, + { + "id": "category-2--subcategory-2-2--item-9", + "category": "Category 2", + "subcategory": "Subcategory 2-2", + "name": "Item 9", + "logo": "logos/cncf.svg", + "description": "This is the description of item 9" + } + ], + "footer": { + "links": { + "facebook": "https://www.facebook.com/CloudNativeComputingFoundation/", + "flickr": "https://www.flickr.com/photos/143247548@N03/albums", + "github": "https://github.com/cncf", + "homepage": "https://cncf.io", + "instagram": "https://www.instagram.com/humans.of.cloudnative/", + "linkedin": "https://www.linkedin.com/company/cloud-native-computing-foundation/", + "slack": "https://slack.cncf.io/", + "twitch": "https://www.twitch.tv/cloudnativefdn", + "twitter": "https://twitter.com/cloudnativefdn", + "wechat": "https://www.cncf.io/wechat/", + "youtube": "https://www.youtube.com/c/cloudnativefdn" + }, + "logo": "https://raw.githubusercontent.com/cncf/artwork/master/other/cncf/horizontal/white/cncf-white.svg", + "text": "

© 2025 DEMO. Privacy Policy ยท Terms of Use

" + }, + "header": { + "links": { + "github": "https://github.com/cncf/landscape2" + }, + "logo": "https://raw.githubusercontent.com/cncf/artwork/master/other/cncf-landscape/horizontal/color/cncf-landscape-horizontal-color.svg" + }, + "featured_items": [ + { + "field": "maturity", + "options": [ + { + "value": "graduated", + "order": 1, + "label": "Graduated" + } + ] + } + ], + "screenshot_width": 1500 +} diff --git a/ui/webapp/tests/e2e/data/full.json b/ui/webapp/tests/e2e/data/full.json new file mode 100644 index 00000000..da38de1f --- /dev/null +++ b/ui/webapp/tests/e2e/data/full.json @@ -0,0 +1,170 @@ +{ + "categories": [ + { + "name": "Category 1", + "normalized_name": "category-1", + "subcategories": [ + { + "name": "Subcategory 1-1", + "normalized_name": "subcategory-1-1" + }, + { + "name": "Subcategory 1-2", + "normalized_name": "subcategory-1-2" + } + ] + }, + { + "name": "Category 2", + "normalized_name": "category-2", + "subcategories": [ + { + "name": "Subcategory 2-1", + "normalized_name": "subcategory-2-1" + }, + { + "name": "Subcategory 2-2", + "normalized_name": "subcategory-2-2" + } + ] + } + ], + "items": [ + { + "id": "category-1--subcategory-1-1--item-1", + "category": "Category 1", + "subcategory": "Subcategory 1-1", + "name": "Item 1", + "logo": "logos/cncf.svg", + "description": "This is the description of item 1", + "maturity": "graduated", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + }, + { + "id": "category-1--subcategory-1-1--item-2", + "category": "Category 1", + "subcategory": "Subcategory 1-1", + "name": "Item 2", + "logo": "logos/cncf.svg", + "description": "This is the description of item 2", + "maturity": "sandbox", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + }, + { + "id": "category-1--subcategory-1-1--item-3", + "category": "Category 1", + "subcategory": "Subcategory 1-1", + "name": "Item 3", + "logo": "logos/cncf.svg", + "description": "This is the description of item 3", + "homepage_url": "https://cncf.io" + }, + { + "id": "category-1--subcategory-1-2--item-4", + "category": "Category 1", + "subcategory": "Subcategory 1-2", + "name": "Item 4", + "logo": "logos/cncf.svg", + "description": "This is the description of item 4", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + }, + { + "id": "category-1--subcategory-1-2--item-5", + "category": "Category 1", + "subcategory": "Subcategory 1-2", + "name": "Item 5", + "logo": "logos/cncf.svg", + "description": "This is the description of item 5", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + }, + { + "id": "category-2--subcategory-2-1--item-6", + "category": "Category 2", + "subcategory": "Subcategory 2-1", + "name": "Item 6", + "logo": "logos/cncf.svg", + "description": "This is the description of item 6", + "maturity": "graduated", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + }, + { + "id": "category-2--subcategory-2-1--item-7", + "category": "Category 2", + "subcategory": "Subcategory 2-1", + "name": "Item 7", + "logo": "logos/cncf.svg", + "description": "This is the description of item 7", + "maturity": "sandbox", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + }, + { + "id": "category-2--subcategory-2-1--item-8", + "category": "Category 2", + "subcategory": "Subcategory 2-1", + "name": "Item 8", + "logo": "logos/cncf.svg", + "description": "This is the description of item 8", + "homepage_url": "https://cncf.io" + }, + { + "id": "category-2--subcategory-2-2--item-9", + "category": "Category 2", + "subcategory": "Subcategory 2-2", + "name": "Item 9", + "logo": "logos/cncf.svg", + "description": "This is the description of item 9", + "homepage_url": "https://cncf.io", + "crunchbase_url": "https://www.crunchbase.com/organization/cloud-native-computing-foundation", + "twitter_url": "https://twitter.com/CloudNativeFdn", + "repositories": [ + { + "url": "https://github.com/cncf/landscape2" + } + ] + } + ], + "crunchbase_data": {}, + "github_data": {} +} diff --git a/ui/webapp/tests/e2e/data/guide.json b/ui/webapp/tests/e2e/data/guide.json new file mode 100644 index 00000000..d4adca9a --- /dev/null +++ b/ui/webapp/tests/e2e/data/guide.json @@ -0,0 +1,32 @@ +{ + "categories": [ + { + "category": "Category 1", + "content": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

", + "subcategories": [ + { + "subcategory": "Subcategory 1-1", + "content": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

" + }, + { + "subcategory": "Subcategory 1-2", + "content": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

" + } + ] + }, + { + "category": "Category 2", + "content": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

", + "subcategories": [ + { + "subcategory": "Subcategory 2-1", + "content": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

" + }, + { + "subcategory": "Subcategory 2-2", + "content": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

" + } + ] + } + ] +} diff --git a/ui/webapp/tests/e2e/data/quiz.json b/ui/webapp/tests/e2e/data/quiz.json new file mode 100644 index 00000000..3e06b0aa --- /dev/null +++ b/ui/webapp/tests/e2e/data/quiz.json @@ -0,0 +1,36 @@ +[ + { + "title": "Which of the following items have feature X?", + "options": [ + { + "item": "Item 1", + "correct": true + }, + { + "item": "Item 2", + "correct": false + }, + { + "item": "Item 3", + "correct": false + } + ] + }, + { + "title": "Please select the item with feature Y", + "options": [ + { + "item": "Item 1", + "correct": false + }, + { + "item": "Item 2", + "correct": false + }, + { + "item": "Item 3", + "correct": true + } + ] + } +] diff --git a/ui/webapp/tests/e2e/data/stats.json b/ui/webapp/tests/e2e/data/stats.json new file mode 100644 index 00000000..22c4f5f2 --- /dev/null +++ b/ui/webapp/tests/e2e/data/stats.json @@ -0,0 +1,110 @@ +{ + "projects": { + "projects": 9, + "maturity": { + "graduated": 2, + "sandbox": 2 + }, + "accepted_at": { + "2024-01": 3, + "2024-02": 3, + "2024-03": 3 + }, + "accepted_at_rt": { + "2024-01": 3, + "2024-02": 6, + "2024-03": 9 + }, + "audits": { + "2024-01": 3, + "2024-02": 3, + "2024-03": 3 + }, + "audits_rt": { + "2024-01": 3, + "2024-02": 6, + "2024-03": 9 + }, + "sandbox_to_incubating": { + "2024-02": 1, + "2024-03": 1 + }, + "incubating_to_graduated": { + "2024-03": 1 + }, + "category": { + "Category 1": { + "projects": 5, + "subcategories": { + "Subcategory 1-1": 3, + "Subcategory 1-2": 2 + } + }, + "Category 2": { + "projects": 4, + "subcategories": { + "Subcategory 2-1": 3, + "Subcategory 2-2": 1 + } + } + }, + "tag": { + "category-1--subcategory-1-1": 3, + "category-1--subcategory-1-2": 2, + "category-2--subcategory-2-1": 3, + "category-2--subcategory-2-2": 1 + } + }, + "members": { + "members": 6, + "subcategories": { + "Category 1": 3, + "Category 2": 3 + }, + "joined_at": { + "2023-01": 3, + "2024-01": 3 + }, + "joined_at_rt": { + "2023-01": 3, + "2024-01": 6 + } + }, + "repositories": { + "repositories": 4, + "contributors": 12, + "stars": 40, + "bytes": 4096, + "languages": { + "TypeScript": 4 + }, + "languages_bytes": { + "TypeScript": 4096 + }, + "participation_stats": [ + 1, + 0, + 2, + 1, + 3, + 2 + ], + "licenses": { + "Apache-2.0": 4 + } + }, + "organizations": { + "funding_rounds": { + "2024": 4 + }, + "funding_rounds_money_raised": { + "2024": 400000 + }, + "acquisitions": { + "2024": 2 + }, + "acquisitions_price": { + "2024": 100000 + } + } +} diff --git a/ui/webapp/tests/e2e/explore.spec.ts b/ui/webapp/tests/e2e/explore.spec.ts new file mode 100644 index 00000000..a8ba010a --- /dev/null +++ b/ui/webapp/tests/e2e/explore.spec.ts @@ -0,0 +1,225 @@ +import { expect, test } from '@playwright/test'; + +import { gotoExplore } from './utils/navigation'; + +test.describe('Explore page', () => { + test('shows navigation and footer content', async ({ page }) => { + await gotoExplore(page); + + // Navigation bar + await expect(page.getByRole('button', { name: 'Go to "Explore" page' }).first()).toBeVisible(); + await expect(page.getByRole('banner').getByText('Explore')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Go to "Guide" page' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Go to "Stats" page' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Open "Embeddable view setup"' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Open Dropdown' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Go to "Games" page' })).toBeVisible(); + await expect(page.getByRole('banner').getByRole('link', { name: 'Open external link' })).toHaveAttribute( + 'href', + 'https://github.com/cncf/landscape2' + ); + + // Footer + await expect(page.getByRole('contentinfo')).toContainText('CNCF interactive landscapes generator'); + await expect(page.getByRole('contentinfo')).toContainText('Privacy Policy'); + await expect(page.getByRole('contentinfo')).toContainText('Terms of Use'); + }); + + test('loads groups and categories', async ({ page }) => { + await gotoExplore(page); + + // Groups + await expect(page.getByRole('button', { name: 'Some categories' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Only category 2' })).toBeVisible(); + + // Categories and subcategories + await expect(page.getByText(/^Category 1/).first()).toBeVisible(); + await expect(page.getByText(/^Category 2/).first()).toBeVisible(); + await expect(page.getByText(/^Subcategory 1-1/).first()).toBeVisible(); + await expect(page.getByText(/^Subcategory 1-2/).first()).toBeVisible(); + await expect(page.getByText(/^Subcategory 2-1/).first()).toBeVisible(); + await expect(page.getByText(/^Subcategory 2-2/).first()).toBeVisible(); + }); + + test('opens second group', async ({ page }) => { + await gotoExplore(page); + + // Clicks on second group + const firstGroupButton = page.getByRole('button', { name: 'Some categories' }); + const secondGroupButton = page.getByRole('button', { name: 'Only category 2' }); + await secondGroupButton.click(); + + // Second group is active and first one becomes inactive + await expect(secondGroupButton).toHaveClass(/active/); + await expect(firstGroupButton).not.toHaveClass(/active/); + }); + + test('adds filters', async ({ page }) => { + await gotoExplore(page); + + // Opens filter modal + const filterButton = page.getByRole('button', { name: 'Open Filters' }); + await filterButton.click(); + + // Expects filter modal to be visible + const filterModal = page.getByLabel('Filters modal'); + await expect(filterModal).toBeVisible(); + await expect(filterModal.locator('div').filter({ hasText: /^Filters$/ }).nth(1)).toBeVisible(); + await expect(filterModal.getByText('Project', { exact: true })).toBeVisible(); + await expect(filterModal.getByLabel('Close')).toBeVisible(); + + // Adds a filter + const filterOption = filterModal.getByLabel('Category 1', { exact: true }); + await filterOption.check(); + await expect(filterOption).toBeChecked(); + + // Applies the filter + const applyButton = filterModal.getByLabel('Apply filters'); + await applyButton.click(); + await expect(filterModal).not.toBeVisible(); + + // Expects filter to be applied + const activeFilter = page.getByRole('listitem').filter({ hasText: 'category:Category 1Clear icon' }); + await expect(activeFilter).toBeVisible(); + await expect(page.getByRole('button', { name: 'Remove Category 1 filter' })).toBeVisible(); + }); + + test('removes filters', async ({ page }) => { + await gotoExplore(page, '?category=Category%201'); + + // Loads with filter applied + const activeFilter = page.getByRole('listitem').filter({ hasText: 'category:Category 1Clear icon' }); + await expect(activeFilter).toBeVisible(); + + // Removes the filter + const removeFilterButton = page.getByRole('button', { name: 'Remove Category 1 filter' }); + await removeFilterButton.click(); + + // Expects filter to be removed + await expect(activeFilter).not.toBeVisible(); + }); + + test('shows no projects message when filters yield no results', async ({ page }) => { + // Loads with filter that yields no results (Category 3 does not exist) + await gotoExplore(page, '?category=Category%203'); + + // Loads with filter applied + const activeFilter = page.getByRole('listitem').filter({ hasText: 'category:Category 3Clear icon' }); + await expect(activeFilter).toBeVisible(); + + // Expects no projects message to be visible + const alert = page.getByRole('alert'); + await expect(alert).toBeVisible(); + await expect(alert.getByText('We couldn\'t find any items that match the criteria selected.')).toBeVisible(); + await expect(alert.getByRole('button', { name: 'Reset filters' })).toBeVisible(); + }); + + test('resets filters from no projects message', async ({ page }) => { + // Loads with filter that yields no results (Category 3 does not exist) + await gotoExplore(page, '?category=Category%203'); + + // Expects no projects message to be visible + const alert = page.getByRole('alert'); + await expect(alert).toBeVisible(); + + // Resets filters + const resetButton = alert.getByRole('button', { name: 'Reset filters' }); + await resetButton.click(); + + // Expects no projects message to be gone and filter to be removed + await expect(alert).not.toBeVisible(); + const activeFilter = page.getByRole('listitem').filter({ hasText: 'category:Category 3Clear icon' }); + await expect(activeFilter).not.toBeVisible(); + }); + + test('shows and hides project details', async ({ page }) => { + await gotoExplore(page); + + // Opens project details + const detailsButton = page.getByRole('button', { name: 'Item 1 info' }); + await detailsButton.click(); + + // Expects project details content to be visible + const detailsSection = page.getByLabel('Item details modal'); + await expect(detailsSection).toBeVisible(); + await expect(detailsSection.getByText('Item 1', { exact: true })).toBeVisible(); + await expect(detailsSection.getByText('DEMO', { exact: true })).toBeVisible(); + await expect(detailsSection.getByText('graduated')).toBeVisible(); + await expect(detailsSection.locator('a').filter({ hasText: 'https://github.com/cncf/' })).toBeVisible(); + await expect(detailsSection.getByText('This is the description of item 1')).toBeVisible(); + await expect(detailsSection.getByText('Repositories')).toBeVisible() + await expect(detailsSection.getByLabel('Repositories select')).toBeVisible(); + await expect(detailsSection.locator('a').filter({ hasText: 'https://github.com/cncf/' })).toBeVisible(); + + // Closes project details + const hideDetailsButton = detailsSection.getByLabel('Close'); + await hideDetailsButton.click(); + await expect(detailsSection).not.toBeVisible(); + }); + + test('displays project dropdown', async ({ page }) => { + await gotoExplore(page); + + // Displays project dropdown + const detailsButton = page.getByRole('button', { name: 'Item 1 info' }); + await detailsButton.hover(); + await page.waitForTimeout(2000); // Wait for the dropdown to appear + + // Expects project dropdown to be visible + const dropdown = page.getByRole('complementary'); + await expect(dropdown).toBeVisible(); + await expect(dropdown.locator('img[alt="Item 1 logo"]')).toBeVisible(); + await expect(dropdown.getByText('Item 1', { exact: true })).toBeVisible(); + await expect(dropdown.getByText('DEMO', { exact: true })).toBeVisible(); + await expect(dropdown.getByText('graduated')).toBeVisible(); + await expect(dropdown.getByText('This is the description of item 1')).toBeVisible(); + }); + + test('displays embeddable view setup modal', async ({ page }) => { + await gotoExplore(page); + + // Opens embeddable view setup modal + const embedButton = page.getByRole('button', { name: 'Open "Embeddable view setup"' }); + await embedButton.click(); + + // Expects embeddable view setup modal to be visible + const embedModal = page.getByLabel('Embeddable view setup modal'); + await expect(embedModal).toBeVisible(); + await expect(embedModal.getByText('Embeddable view setup', { exact: true })).toBeVisible(); + await expect(embedModal.getByText('Embed code')).toBeVisible(); + await expect(embedModal.getByLabel('Classification options')).toBeVisible(); + await expect(embedModal.getByLabel('Categories list', { exact: true })).toBeVisible(); + await expect(embedModal.getByLabel('Subcategories list')).toBeVisible(); + await expect(embedModal.getByLabel('Copy code to clipboard')).toBeVisible(); + + // Closes embeddable view setup modal + const closeButton = embedModal.getByLabel('Close modal'); + await closeButton.click(); + await expect(embedModal).not.toBeVisible(); + }); + + test('downloads landscape image', async ({ page }) => { + await gotoExplore(page); + + // Clicks on download image button + const downloadButton = page.getByRole('button', { name: 'Open dropdown' }) + await downloadButton.click(); + + // Expects download to be open + const dropdown = page.getByRole('complementary') + await expect(dropdown).toBeVisible(); + await expect(dropdown.getByRole('button', { name: 'Download landscape in PDF' })).toBeVisible(); + await expect(dropdown.getByRole('button', { name: 'Download landscape in PNG' })).toBeVisible(); + await expect(dropdown.getByRole('button', { name: 'Download items in CSV format' })).toBeVisible(); + await expect(dropdown.getByRole('button', { name: 'Download projects in CSV' })).toBeVisible(); + + // Downloads landscape PNG + const [download] = await Promise.all([ + page.waitForEvent('download'), + dropdown.getByRole('button', { name: 'Download landscape in PNG' }).click(), + ]); + + // Expects download to have correct filename + expect(download.suggestedFilename()).toBe('landscape.png'); + }); +}); diff --git a/ui/webapp/tests/e2e/games.spec.ts b/ui/webapp/tests/e2e/games.spec.ts new file mode 100644 index 00000000..cd30dfd8 --- /dev/null +++ b/ui/webapp/tests/e2e/games.spec.ts @@ -0,0 +1,138 @@ +import { expect, Page, test } from '@playwright/test'; + +import { gotoExplore, gotoGames } from './utils/navigation'; +import quizData from './data/quiz.json' assert { type: 'json' }; + +type QuizFixture = { + title: string; + options: { + item: string; + correct: boolean; + }[]; +}; + +const quizFixtures = quizData as QuizFixture[]; +const totalQuestions = quizFixtures.length; + +const startQuiz = async (page: Page) => { + const startGameButton = page.getByRole('button', { name: 'Start game' }); + await expect(startGameButton).toBeVisible(); + await startGameButton.click(); + await expect(page.getByLabel(`question 1 of ${totalQuestions}`)).toBeVisible(); +}; + +const getActiveQuestion = async (page: Page) => { + const title = (await page.getByTestId('quiz-question-title').innerText()).trim(); + const question = quizFixtures.find((fixture) => fixture.title === title); + expect(question, `Unknown quiz question "${title}"`).toBeDefined(); + return question!; +}; + +const answerCurrentQuestion = async (page: Page, shouldAnswerCorrect: boolean) => { + const question = await getActiveQuestion(page); + const option = shouldAnswerCorrect + ? question.options.find((candidate) => candidate.correct) + : question.options.find((candidate) => !candidate.correct); + expect(option, 'Missing quiz option for desired correctness').toBeDefined(); + await page.getByRole('button', { name: option!.item }).click(); +}; + +const goToNextQuestion = async (page: Page, nextIndex: number) => { + const nextButton = page.getByRole('button', { name: 'Next' }); + await expect(nextButton).toBeEnabled(); + await nextButton.click(); + await expect(page.getByLabel(`question ${nextIndex} of ${totalQuestions}`)).toBeVisible(); +}; + +const expectScore = async (page: Page, correct: number, wrong: number) => { + await expect(page.locator(`[aria-label="${correct} correct guesses"]`)).toBeVisible(); + await expect(page.locator(`[aria-label="${wrong} wrong guesses"]`)).toBeVisible(); +}; + +test.describe('Games page', () => { + test('loads games content when navigating from the header', async ({ page }) => { + await gotoExplore(page); + await Promise.all([ + page.waitForURL(/\/games/), + page.getByRole('button', { name: 'Go to "Games" page' }).click(), + ]); + await expect(page).toHaveURL(/\/games/); + + // Games landing state + const startGameButton = page.getByRole('button', { name: 'Start game' }); + await expect(startGameButton).toBeVisible(); + await expect(page.getByText('Landscape Quiz').nth(1)).toBeVisible(); + + // Starts the quiz + await startGameButton.click(); + + // Quiz question and options + await expect(page.getByLabel('question 1 of 2')).toBeVisible(); + await expect( + page.getByText(/Which of the following items have feature X\?|Please select the item with feature Y/) + ).toBeVisible(); + await expect(page.getByRole('button', { name: 'Item 1' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Item 2' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Item 3' })).toBeVisible(); + }); + + test('loads games page', async ({ page }) => { + await gotoGames(page); + + // Games landing state + const startGameButton = page.getByRole('button', { name: 'Start game' }); + await expect(startGameButton).toBeVisible(); + + // Starts the quiz + await startGameButton.click(); + + // Quiz question and options + await expect(page.getByLabel('question 1 of 2')).toBeVisible(); + await expect( + page.getByText(/Which of the following items have feature X\?|Please select the item with feature Y/) + ).toBeVisible(); + await expect(page.getByRole('button', { name: 'Item 1' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Item 2' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Item 3' })).toBeVisible(); + }); + + test('updates score as questions are answered', async ({ page }) => { + await gotoGames(page); + await startQuiz(page); + + // Answer the first question correctly + await answerCurrentQuestion(page, true); + await expectScore(page, 1, 0); + await goToNextQuestion(page, 2); + + // Answer the second question incorrectly + await answerCurrentQuestion(page, false); + await expectScore(page, 1, 1); + await expect(page.getByRole('button', { name: 'Play again' })).toBeEnabled(); + await expect(page.getByRole('button', { name: 'Next' })).toHaveCount(0); + }); + + test('restarts quiz state after finishing a game', async ({ page }) => { + await gotoGames(page); + await startQuiz(page); + + // Answer all questions correctly + for (let index = 1; index <= totalQuestions; index += 1) { + await answerCurrentQuestion(page, true); + await expectScore(page, index, 0); + if (index < totalQuestions) { + await goToNextQuestion(page, index + 1); + } + } + + // Restart the quiz + const playAgainButton = page.getByRole('button', { name: 'Play again' }); + await expect(playAgainButton).toBeEnabled(); + await playAgainButton.click(); + + // Verify quiz state reset + await expectScore(page, 0, 0); + await expect(page.getByLabel(`question 1 of ${totalQuestions}`)).toBeVisible(); + await expect(page.getByRole('button', { name: 'Next' })).toBeDisabled(); + }); +}); diff --git a/ui/webapp/tests/e2e/guide.spec.ts b/ui/webapp/tests/e2e/guide.spec.ts new file mode 100644 index 00000000..080e2068 --- /dev/null +++ b/ui/webapp/tests/e2e/guide.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from '@playwright/test'; + +import { gotoExplore, gotoGuide, waitForGuideData } from './utils/navigation'; + +test.describe('Guide page', () => { + test('loads guide content when navigating from the header', async ({ page }) => { + await gotoExplore(page); + await Promise.all([waitForGuideData(page), page.getByRole('button', { name: 'Go to "Guide" page' }).click()]); + await expect(page).toHaveURL(/\/guide/); + + // Guide headings + await expect(page.getByRole('heading', { name: 'Category 1', exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Subcategory 1-1' })).toBeVisible(); + }); + + test('loads guide page', async ({ page }) => { + await gotoGuide(page); + + // Guide headings + await expect(page.getByRole('heading', { name: 'Category 1', exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Subcategory 1-1' })).toBeVisible(); + }); + + test('navigates via the table of contents to another category', async ({ page }) => { + await gotoGuide(page); + + await page.getByRole('button', { name: 'Open Category 2 section' }).click(); + + // Verify URL hash and content visibility + await expect(page).toHaveURL(/#category-2$/); + await expect( + page.locator('#section_category-2').getByRole('heading', { level: 1, name: 'Category 2' }) + ).toBeVisible(); + }); + + test('loads subcategory content when visiting the guide with a hash', async ({ page }) => { + await gotoGuide(page, 'category-2--subcategory-2-1'); + + await expect(page).toHaveURL(/#category-2--subcategory-2-1$/); + + // Verify subcategory content visibility + const subcategorySection = page.locator('#section_category-2--subcategory-2-1'); + await expect(subcategorySection.getByRole('heading', { level: 2, name: 'Subcategory 2-1' })).toBeVisible(); + }); + + test('loads projects list in guide content', async ({ page }) => { + await gotoGuide(page); + + // Verify project table and details visibility + const subcategorySection = page.locator('#section_category-1--subcategory-1-1'); + const projectTable = subcategorySection.getByRole('table').first(); + await expect(projectTable).toContainText('DEMO Projects'); + await expect(projectTable).toContainText('Item 1 (graduated)'); + await expect(projectTable).toContainText('Item 2 (sandbox)'); + }); + + test('opens project details modal', async ({ page }) => { + await gotoGuide(page); + + // Opens project details + const detailsButton = page.getByRole('button', { name: 'Item 1 info' }); + await detailsButton.click(); + + // Open project details modal + const detailsSection = page.getByLabel('Item details modal'); + await expect(detailsSection).toBeVisible(); + await expect(detailsSection.getByText('Item 1', { exact: true })).toBeVisible(); + await expect(detailsSection.getByText('DEMO', { exact: true })).toBeVisible(); + await expect(detailsSection.getByText('graduated')).toBeVisible(); + await expect(detailsSection.locator('a').filter({ hasText: 'https://github.com/cncf/' })).toBeVisible(); + await expect(detailsSection.getByText('This is the description of item 1')).toBeVisible(); + await expect(detailsSection.getByText('Repositories')).toBeVisible() + await expect(detailsSection.getByLabel('Repositories select')).toBeVisible(); + await expect(detailsSection.locator('a').filter({ hasText: 'https://github.com/cncf/' })).toBeVisible(); + }); +}); diff --git a/ui/webapp/tests/e2e/stats.spec.ts b/ui/webapp/tests/e2e/stats.spec.ts new file mode 100644 index 00000000..4874b18d --- /dev/null +++ b/ui/webapp/tests/e2e/stats.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; + +import { gotoExplore, gotoStats } from './utils/navigation'; + +test.describe('Stats page', () => { + test('navigates to stats view from the header', async ({ page }) => { + await gotoExplore(page); + await page.getByRole('button', { name: 'Go to "Stats" page' }).click(); + await expect(page).toHaveURL(/\/stats$/); + + // Subtitles in stats page + await expect(page.getByText('Distribution by maturity')).toBeVisible(); + await expect(page.getByText('Accepted over time')).toBeVisible(); + await expect(page.getByText('Promotions')).toBeVisible(); + await expect(page.getByText('Security audits')).toBeVisible(); + await expect(page.getByText('Distribution by category, subcategory and TAG')).toBeVisible(); + await expect(page.getByText('Distribution by category', { exact: true })).toBeVisible(); + await expect(page.getByText('Memberships over time')).toBeVisible(); + await expect(page.getByText('Most popular languages')).toBeVisible(); + await expect(page.getByText('Activity')).toBeVisible(); + await expect(page.getByText('Licenses')).toBeVisible(); + await expect(page.getByText('Funding rounds', { exact: true })).toBeVisible(); + await expect(page.getByText('Acquisitions', { exact: true })).toBeVisible(); + }); + + test('toggles category distribution rows', async ({ page }) => { + await gotoStats(page); + + const categoryTable = page.getByRole('treegrid'); + const categoryOneRow = categoryTable.locator('tbody tr').filter({ hasText: 'Category 1' }).first(); + const categoryTwoRow = categoryTable.locator('tbody tr').filter({ hasText: 'Category 2' }).first(); + + await expect(categoryTable.getByText('Subcategory 1-1')).toBeVisible(); + await expect(categoryTable.getByText('Subcategory 2-1')).not.toBeVisible(); + + // Collapse and expand category rows + await categoryOneRow.click(); + await expect(categoryTable.getByText('Subcategory 1-1')).not.toBeVisible(); + await categoryTwoRow.click(); + await expect(categoryTable.getByText('Subcategory 2-1')).toBeVisible(); + }); + + test('lists repository licenses with counts and percentages', async ({ page }) => { + await gotoStats(page); + + // Verify licenses table and details visibility + const licensesTable = page.locator('table').filter({ hasText: 'License' }).first(); + const apacheRow = licensesTable.getByRole('row', { name: /Apache-2\.0/ }); + + await expect(apacheRow).toContainText('4'); + await expect(apacheRow).toContainText('100.00%'); + }); +}); diff --git a/ui/webapp/tests/e2e/utils/navigation.ts b/ui/webapp/tests/e2e/utils/navigation.ts new file mode 100644 index 00000000..494668cb --- /dev/null +++ b/ui/webapp/tests/e2e/utils/navigation.ts @@ -0,0 +1,100 @@ +import { Page, type Route } from '@playwright/test'; + +import baseData from '../data/base.json' assert { type: 'json' }; +import fullData from '../data/full.json' assert { type: 'json' }; +import guideData from '../data/guide.json' assert { type: 'json' }; +import quizData from '../data/quiz.json' assert { type: 'json' }; +import statsData from '../data/stats.json' assert { type: 'json' }; + +const placeholderLogoSvg = + ''; + +const registeredPages = new WeakSet(); + +const fixtures = { + base: baseData, + full: fullData, + guide: guideData, + quiz: quizData, + stats: statsData, +}; + +// Helper to fulfill a route with JSON data +const fulfillJson = (route: Route, data: unknown) => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(data), + }); +}; + +// Register routes to serve template data fixtures +const registerTemplateRoutes = async (page: Page) => { + if (registeredPages.has(page)) { + return; + } + registeredPages.add(page); + + await Promise.all([ + page.route('**/static/data/base.json', (route) => fulfillJson(route, fixtures.base)), + page.route('**/static/data/stats.json', (route) => fulfillJson(route, fixtures.stats)), + page.route('**/static/data/guide.json', (route) => fulfillJson(route, fixtures.guide)), + page.route('**/static/data/full.json', (route) => fulfillJson(route, fixtures.full)), + page.route('**/static/data/quiz.json', (route) => fulfillJson(route, fixtures.quiz)), + page.route('**/static/logos/**', (route) => + route.fulfill({ status: 200, contentType: 'image/svg+xml', body: placeholderLogoSvg }) + ), + ]); +}; + +// Navigate to the explore page, registering template routes beforehand +export const gotoExplore = async (page: Page, query?: string) => { + await registerTemplateRoutes(page); + await Promise.all([ + page.waitForResponse((response) => response.url().includes('/static/data/base.json') && response.ok()), + page.goto(`/${query || ''}`), + ]); +}; + +// Navigate to the stats page, registering template routes beforehand +export const gotoStats = async (page: Page) => { + await registerTemplateRoutes(page); + await Promise.all([ + page.waitForResponse((response) => response.url().includes('/static/data/stats.json') && response.ok()), + page.goto('/stats'), + ]); +}; + +// Navigate to the guide page (optionally with a hash) after registering routes +export const gotoGuide = async (page: Page, hash?: string) => { + await registerTemplateRoutes(page); + const normalizedHash = hash?.startsWith('#') ? hash.slice(1) : hash; + const target = normalizedHash ? `/guide#${normalizedHash}` : '/guide'; + await Promise.all([ + page.waitForResponse((response) => response.url().includes('/static/data/guide.json') && response.ok()), + page.goto(target), + ]); +}; + +// Navigate to the games page, registering template routes beforehand +export const gotoGames = async (page: Page) => { + await registerTemplateRoutes(page); + await Promise.all([ + page.waitForResponse((response) => response.url().includes('/static/data/quiz.json') && response.ok()), + page.goto('/games'), + ]); +}; + +// Wait for guide data to be loaded +export const waitForGuideData = async (page: Page) => { + await page.waitForResponse((response) => { + if (!response.url().includes('/static/data/guide.json')) { + return false; + } + const request = response.request(); + if (request.method() !== 'GET') { + return false; + } + return response.ok(); + }); +}; diff --git a/ui/webapp/vite.config.ts b/ui/webapp/vite.config.ts index 47ece53e..e13be327 100644 --- a/ui/webapp/vite.config.ts +++ b/ui/webapp/vite.config.ts @@ -82,6 +82,8 @@ export default defineConfig({ preprocessorOptions: { scss: { includePaths: scssIncludePaths, + quietDeps: true, + silenceDeprecations: ['import', 'global-builtin', 'color-functions'], }, }, }, diff --git a/ui/webapp/yarn.lock b/ui/webapp/yarn.lock index e0ad5f05..a03e649c 100644 --- a/ui/webapp/yarn.lock +++ b/ui/webapp/yarn.lock @@ -981,6 +981,13 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== +"@playwright/test@^1.56.1": + version "1.56.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" + integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== + dependencies: + playwright "1.56.1" + "@rollup/rollup-android-arm-eabi@4.52.2": version "4.52.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz#3a43e904367cd6147c5a8de9df4ff7ffa48634ec" @@ -2295,6 +2302,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@^2.3.3, fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -3454,6 +3466,20 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" + integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== + +playwright@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" + integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== + dependencies: + playwright-core "1.56.1" + optionalDependencies: + fsevents "2.3.2" + postcss@^8.3.11, postcss@^8.5.6: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"