Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# Use port 80 to go through Nginx (proxy service on Docker)
VITE_API_BASE_URL=http://localhost:8000
VITE_FEATURE_FLAG_SHOW_DEV=false
PLAYWRIGHT_TEST_BASE_URL=https://staging.dashboard.kernelci.org:9000
4 changes: 4 additions & 0 deletions dashboard/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ dist-ssr
# eslint cache
.eslintcache

# Playwright test reports and artifacts
test-results/
playwright-report/

.env*
!.env.example
28 changes: 27 additions & 1 deletion dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,38 @@ pnpm dev
```

## Running unit tests

The frontend includes unit tests covering some parts of the source code. To run the tests, use the following command:

```sh
pnpm test
```

## Running end-to-end (e2e) tests

The project includes Playwright-based end-to-end tests. To run the tests, first set the test environment URL in your .env file:

```sh
# Copy the example file
cp .env.example .env

# Edit the .env file to set PLAYWRIGHT_TEST_BASE_URL to your desired environment
# Available environments:
# - Staging: https://staging.dashboard.kernelci.org:9000 (default)
# - Production: https://dashboard.kernelci.org
# - Local: http://localhost:5173
```

Then run the e2e tests:

```sh
# Run all e2e tests
pnpm run e2e

# Run e2e tests with UI mode for debugging
pnpm run e2e-ui
```

# Routing and State Management

A big part of this project is to have shareable links
Expand All @@ -37,5 +63,5 @@ Also, we are using file based routing in the tanstack router, only files that st
# Feature Flags

They are used when we want to hide a feature for some users, without having to do branch manipulation.
Right now the only feature flag is for Dev only and it is controlled by the env
Right now the only feature flag is for Dev only and it is controlled by the env
`FEATURE_FLAG_SHOW_DEV=false` it is a boolean.
26 changes: 26 additions & 0 deletions dashboard/e2e/e2e-selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const TREE_LISTING_SELECTORS = {
table: 'table',
treeColumnHeader: 'th button:has-text("Tree")',
branchColumnHeader: 'th button:has-text("Branch")',

intervalInput: 'input[type="number"][min="1"]',

// This requires nth() selector which can't be stored as string
itemsPerPageDropdown: '[role="listbox"]',
itemsPerPageOption: (value: string) => `[role="option"]:has-text("${value}")`,

searchInput: 'input[type="text"]',

nextPageButton: '[role="button"]:has-text(">")',
previousPageButton: '[role="button"]:has-text("<")',

treeNameCell: (treeName: string) => `td a:has-text("${treeName}")`,
firstTreeCell: 'td a',

breadcrumbTreesLink: '[role="link"]:has-text("Trees")',
} as const;

export const COMMON_SELECTORS = {
tableRow: 'tr',
tableHeader: 'th',
} as const;
116 changes: 116 additions & 0 deletions dashboard/e2e/tree-listing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { test, expect } from '@playwright/test';

import { TREE_LISTING_SELECTORS, COMMON_SELECTORS } from './e2e-selectors';

const PAGE_LOAD_TIMEOUT = 5000;
const DEFAULT_ACTION_TIMEOUT = 1000;
const SEARCH_UPDATE_TIMEOUT = 2000;
const NAVIGATION_TIMEOUT = 5000;
const GO_BACK_TIMEOUT = 3000;

test.describe('Tree Listing Page Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tree');
await page.waitForTimeout(PAGE_LOAD_TIMEOUT);
});

test('loads tree listing page correctly', async ({ page }) => {
await expect(page).toHaveTitle(/KernelCI/);
await expect(page).toHaveURL(/\/tree/);

await expect(page.locator(TREE_LISTING_SELECTORS.table)).toBeVisible();

await expect(
page.locator(TREE_LISTING_SELECTORS.treeColumnHeader),
).toBeVisible();
await expect(
page.locator(TREE_LISTING_SELECTORS.branchColumnHeader),
).toBeVisible();
});

test('change time interval', async ({ page }) => {
await expect(page.locator(COMMON_SELECTORS.tableRow).first()).toBeVisible();

const intervalInput = page
.locator(TREE_LISTING_SELECTORS.intervalInput)
.first();
await expect(intervalInput).toBeVisible();

await intervalInput.fill('7');

await page.waitForTimeout(DEFAULT_ACTION_TIMEOUT);

await expect(intervalInput).toHaveValue('7');
});

test('change table size', async ({ page }) => {
await expect(page.locator(TREE_LISTING_SELECTORS.table)).toBeVisible();

const tableSizeSelector = page.locator('[role="combobox"]').nth(1);
await expect(tableSizeSelector).toBeVisible();

await tableSizeSelector.click();

await expect(
page.locator(TREE_LISTING_SELECTORS.itemsPerPageDropdown),
).toBeVisible();

await page.locator(TREE_LISTING_SELECTORS.itemsPerPageOption('20')).click();

await page.waitForTimeout(DEFAULT_ACTION_TIMEOUT);

await expect(tableSizeSelector).toContainText('20');
});

test('search for trees', async ({ page }) => {
const searchInput = page.locator(TREE_LISTING_SELECTORS.searchInput).nth(0);
await expect(searchInput).toBeVisible();
await searchInput.fill('main');

await page.waitForTimeout(SEARCH_UPDATE_TIMEOUT);

const tableRows = page.locator(COMMON_SELECTORS.tableRow);
const count = await tableRows.count();
expect(count).toBeGreaterThan(1);
});

test('navigate to tree details and back via breadcrumb', async ({ page }) => {
await expect(page.locator(TREE_LISTING_SELECTORS.table)).toBeVisible();

const firstTreeLink = page.locator('td a').first();
await expect(firstTreeLink).toBeVisible();

await firstTreeLink.click();

await page.waitForTimeout(NAVIGATION_TIMEOUT);

const url = page.url();
expect(url).toMatch(/\/tree\/[^/]+\/[^/]+\/[^/]+$/);

await page.goBack();
await page.waitForTimeout(GO_BACK_TIMEOUT);

await expect(page).toHaveURL(/\/tree$/);
});

test('pagination navigation', async ({ page }) => {
await expect(page.locator(TREE_LISTING_SELECTORS.table)).toBeVisible();

const nextPageButton = page
.locator(TREE_LISTING_SELECTORS.nextPageButton)
.first();
const hasNextPage =
(await nextPageButton.count()) > 0 &&
!(await nextPageButton.isDisabled());

if (hasNextPage) {
const originalPageUrl = page.url();
await nextPageButton.click();

await page.waitForTimeout(SEARCH_UPDATE_TIMEOUT);

const newPageUrl = page.url();
expect(newPageUrl).not.toBe(originalPageUrl);
}
});
});
3 changes: 2 additions & 1 deletion dashboard/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default [{
},

requireConfigFile: false,
project: ["./tsconfig.app.json", "./tsconfig.node.json"],
project: ["./tsconfig.app.json", "./tsconfig.node.json", "./tsconfig.e2e.json"],
tsconfigRootDir: __dirname,
}
},
Expand Down Expand Up @@ -137,6 +137,7 @@ export default [{
".storybook/**",
"src/stories/**",
"**/*.stories*",
"playwright.config.ts",
],
}],

Expand Down
5 changes: 4 additions & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"prepare": "cd .. && husky dashboard/.husky",
"pycommit": "cd .. && cd backend && sh pre-commit",
"pypush": "cd .. && cd backend && sh pre-push",
"prettify": "prettier --write ./src"
"prettify": "prettier --write ./src ./e2e",
"e2e": "playwright test",
"e2e-ui": "playwright test --ui"
},
"dependencies": {
"@date-fns/tz": "^1.4.1",
Expand Down Expand Up @@ -77,6 +79,7 @@
"@eslint/compat": "^1.3.2",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.34.0",
"@playwright/test": "^1.57.0",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-interactions": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
Expand Down
8 changes: 8 additions & 0 deletions dashboard/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
use: {
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:5173',
},
});
38 changes: 38 additions & 0 deletions dashboard/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions dashboard/tsconfig.e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.e2e.tsbuildinfo",
"skipLibCheck": false,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["@playwright/test", "node"]
},
"include": ["e2e/**/*"]
}
3 changes: 3 additions & 0 deletions dashboard/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
},
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.e2e.json"
}
],
"compilerOptions": {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts", "playwright.config.ts", "vitest.config.ts"]
}
20 changes: 20 additions & 0 deletions dashboard/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable import/no-extraneous-dependencies */
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/e2e/**',
'**/.{idea,git,cache,output,temp}/**',
],
},
});