Skip to content

Commit

Permalink
Add frontend tests with playwright (#256)
Browse files Browse the repository at this point in the history
* Add frontend tests with playwright

The tests are written in python

Add a Pipeline for github.

* fix tests

* fix pipeline

* fix pipeline

* Remove checked in vscode folder

* refactor: autofix issues in 1 file

Resolved issues in ui/tests/test_ui.py with DeepSource Autofix

* Configure more tests location for python code

* Revert "refactor: autofix issues in 1 file"

This reverts commit f9970b2.

assert in test code is OK in python, so revert this.

* Simplify assertion

---------

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Co-authored-by: Felix Schumacher <felix@internetallee.de>
Co-authored-by: Felix Schumacher <felix.schumacher@internetallee.de>
  • Loading branch information
4 people authored Nov 10, 2024
1 parent 4f4cd04 commit a9b128d
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 42 deletions.
1 change: 1 addition & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ version = 1

test_patterns = [
"api/tests/**",
"ui/tests/**",
"test_*.py"
]

Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Run Python Playwright Tests
on:
pull_request:
types:
- opened
- reopened
- synchronize
push:
branches:
- main
- master

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.12'

- name: Install dependencies
run: |
cd api
pip install .
- name: Install Playwright Browsers
run: |
cd api
playwright install
- name: Start Vue.js Development Server
run: |
cd ui
npm install
nohup npm run dev &
sleep 5
- name: Run Playwright Tests
run: |
cd ui
pytest tests/
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
version:
description: 'Contains application version'
required: true

jobs:
build:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ __pycache__/
.mypy_cache/
.pytest_cache/
.ruff_cache/
.vscode/
venv/
app/static/.webassets-cache/
app/static/screen.css
Expand Down
209 changes: 173 additions & 36 deletions api/poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ JSON-log-formatter = "^0.5.1"
uvicorn = "^0.20.0"
pydantic = "^2.0.0"
pydantic-settings = "^2.0.3"
playwright = "^1.47.0"
pytest-playwright = "^0.5.2"

[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
Expand Down
1 change: 1 addition & 0 deletions ui/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_MOCK_NAMESPACES=true
39 changes: 34 additions & 5 deletions ui/src/components/Secrets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,30 @@ const sealedSecretsAnnotations = computed(() => {
return `{ sealedsecrets.bitnami.com/${scope.value}: "true" }`;
})
const adjectives = [
"altered", "angry", "big", "blinking", "boring", "broken", "bubbling", "calculating",
"cute", "diffing", "expensive", "fresh", "fierce", "floating", "generous", "golden",
"green", "growing", "hidden", "hideous", "interesting", "kubed", "mumbling", "rusty",
"singing", "small", "sniffing", "squared", "talking", "trusty", "wise", "walking", "zooming"
];
const nouns = [
"ant", "bike", "bird", "captain", "cheese", "clock", "digit", "gorilla", "kraken", "number",
"maven", "monitor", "moose", "moon", "mouse", "news", "newt", "octopus", "opossum", "otter",
"paper", "passenger", "potato", "ship", "spaceship", "spaghetti", "spoon", "store", "tomcat",
"trombone", "unicorn", "vine", "whale"
];
function mockNamespacesResolver(count) {
const randomPairs = new Set();
while (randomPairs.size < count) {
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
randomPairs.add(`${adjective}-${noun}`);
}
return Array.from(randomPairs).sort();
}
function setErrorMessage(newErrorMessage) {
errorMessage.value = newErrorMessage;
hasErrorMessage.value = !!newErrorMessage;
Expand All @@ -429,14 +453,19 @@ async function fetchConfig() {
}
async function fetchNamespaces(config) {
try {
const response = await fetch(`${config.api_url}/namespaces`);
namespaces.value = await response.json();
} catch (error) {
setErrorMessage(error);
if (import.meta.env.VITE_MOCK_NAMESPACES) {
namespaces.value = mockNamespacesResolver(10);
} else {
try {
const response = await fetch(`${config.api_url}/namespaces`);
namespaces.value = await response.json();
} catch (error) {
setErrorMessage(error);
}
}
}
async function fetchDisplayName(config) {
displayName.value = config.display_name;
}
Expand Down
169 changes: 169 additions & 0 deletions ui/tests/test_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import os
from playwright.sync_api import sync_playwright, Page


def test_ui_start():
with sync_playwright() as ctx:
browser = ctx.chromium.launch(headless=True)
page = browser.new_page()
page.goto("http://localhost:8080")
page.wait_for_load_state("load")
assert page.title() == "Kubeseal Webgui"
browser.close()


def test_secret_form_with_value():
with sync_playwright() as ctx:
browser = ctx.chromium.launch(headless=True)
page = browser.new_page()
page.goto("http://localhost:8080")
page.wait_for_load_state("load")

disabled_encrypt_button(page)
namespace_select(page)
secret_name(page)
scope_strict(page)
add_secret_key_value(page)
click_encrypt_button(page)

browser.close()


def test_secret_form_with_file():
with sync_playwright() as ctx:
browser = ctx.chromium.launch(headless=True)
page = browser.new_page()
page.goto("http://localhost:8080")
page.wait_for_load_state("load")

disabled_encrypt_button(page)
namespace_select(page)
secret_name(page)
scope_strict(page)
add_secret_key_file(page)
click_encrypt_button(page)

browser.close()


def test_secret_form_with_invalid_file():
with sync_playwright() as ctx:
browser = ctx.chromium.launch(headless=True)
page = browser.new_page()
page.goto("http://localhost:8080")
page.wait_for_load_state("load")

file_input = page.locator('input[type="file"]#input-16')
# Try to upload a non-valid file
invalid_file_path = os.path.join(os.getcwd(), "large_test_file.txt")
with open(invalid_file_path, "w") as f:
f.write(
"X" * (10 * 1024 * 1024)
) # Create a 10MB file (assuming it's too large)
file_input.set_input_files(invalid_file_path)

# Check for an error message (adjust the selector as needed)
error_message = page.locator(
"text='File size should be less than 1 MB!'"
) # Replace with actual error message
assert (
error_message.is_visible()
), "Error message should be visible for invalid file"
if os.path.exists(invalid_file_path):
os.remove(invalid_file_path)
browser.close()


def namespace_select(page: Page):
input_selector = "input#input-4"
page.wait_for_selector(input_selector, timeout=10000)
page.click(input_selector)
suggestions = page.query_selector_all(".v-list-item-title")
assert len(suggestions) > 0, "No suggestions found."
first_suggestion_text = suggestions[0].inner_text()
suggestions[0].click()
selected_value = page.input_value(input_selector)
assert (
selected_value == first_suggestion_text
), f"Expected '{first_suggestion_text}', but got '{selected_value}'"


def secret_name(page: Page):
input_selector = "#input-secret-name"
page.wait_for_selector(input_selector)
input_text = "valid-secret-name"
page.fill(input_selector, input_text)
assert (
page.input_value(input_selector) == input_text
), f"Expected {input_text}, but got {page.input_value(input_selector)}"


def scope_strict(page: Page):
select_selector = "div.v-select"
page.wait_for_selector(select_selector)
page.click(select_selector)
item_selector = "div.v-list-item"
page.wait_for_selector(item_selector)
items = page.query_selector_all(item_selector)
assert len(items) > 0, "No items found in the dropdown."
for item in items:
title_element = item.query_selector("div.v-list-item-title")
if title_element and title_element.inner_text().strip() == "strict":
item.click()
break


def add_secret_key_value(page: Page):
page.wait_for_selector("textarea#input-12")
page.fill("textarea#input-12", "my-secret-key")
assert page.locator("textarea#input-12").input_value() == "my-secret-key"
page.wait_for_selector("textarea#input-14")
page.fill("textarea#input-14", "my-secret-value")
assert page.locator("textarea#input-14").input_value() == "my-secret-value"
file_input = page.locator('input[type="file"]#input-16')
assert (
not file_input.is_enabled()
), "File input should be disabled when value is filled"


def add_secret_key_file(page: Page):
page.wait_for_selector("textarea#input-12")
page.fill("textarea#input-12", "my-secret-key")
assert page.locator("textarea#input-12").input_value() == "my-secret-key"

file_input = page.locator('input[type="file"]#input-16')
assert file_input.is_visible()
test_file_path = os.path.join(os.getcwd(), "test_file.txt")
with open(test_file_path, "w") as f:
f.write("This is a test file.")
file_input.set_input_files(test_file_path)
if os.path.exists(test_file_path):
os.remove(test_file_path)


def disabled_encrypt_button(page: Page):
encrypt_button = page.locator('button:has-text("Encrypt")')
encrypt_button.wait_for()
assert encrypt_button.is_disabled()


def click_encrypt_button(page: Page):
# Mock the fetchEncodedSecrets function by overriding it in the browser context
page.evaluate(
"""
const appElement = document.querySelector('#app');
const vueApp = appElement.__vue_app__;
if (vueApp) {
vueApp._instance.proxy.fetchEncodedSecrets = function() {
window.fetchEncodedSecretsCalled = true;
};
}
"""
)
encrypt_button = page.locator('button:has-text("Encrypt")')
encrypt_button.wait_for()
assert encrypt_button.is_enabled()
encrypt_button.click()
is_called = page.evaluate("window.fetchEncodedSecretsCalled === true")
assert is_called, "fetchEncodedSecrets function was not called!"

0 comments on commit a9b128d

Please sign in to comment.