mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
fix(ui): stabilize cloud e2e prerequisites (#12024)
This commit is contained in:
@@ -249,6 +249,7 @@ modules:
|
|||||||
- ui/tests/profile/**
|
- ui/tests/profile/**
|
||||||
- ui/tests/lighthouse/**
|
- ui/tests/lighthouse/**
|
||||||
- ui/tests/home/**
|
- ui/tests/home/**
|
||||||
|
- ui/tests/navigation/**
|
||||||
- ui/tests/attack-paths/**
|
- ui/tests/attack-paths/**
|
||||||
|
|
||||||
- name: api-serializers
|
- name: api-serializers
|
||||||
@@ -275,6 +276,7 @@ modules:
|
|||||||
- ui/tests/profile/**
|
- ui/tests/profile/**
|
||||||
- ui/tests/lighthouse/**
|
- ui/tests/lighthouse/**
|
||||||
- ui/tests/home/**
|
- ui/tests/home/**
|
||||||
|
- ui/tests/navigation/**
|
||||||
- ui/tests/attack-paths/**
|
- ui/tests/attack-paths/**
|
||||||
|
|
||||||
- name: api-filters
|
- name: api-filters
|
||||||
@@ -432,6 +434,14 @@ modules:
|
|||||||
e2e:
|
e2e:
|
||||||
- ui/tests/lighthouse/**
|
- ui/tests/lighthouse/**
|
||||||
|
|
||||||
|
- name: ui-navigation
|
||||||
|
match:
|
||||||
|
- ui/components/layout/**
|
||||||
|
- ui/tests/navigation/**
|
||||||
|
tests: []
|
||||||
|
e2e:
|
||||||
|
- ui/tests/navigation/**
|
||||||
|
|
||||||
- name: ui-overview
|
- name: ui-overview
|
||||||
match:
|
match:
|
||||||
- ui/components/overview/**
|
- ui/components/overview/**
|
||||||
@@ -464,6 +474,7 @@ modules:
|
|||||||
- ui/tests/profile/**
|
- ui/tests/profile/**
|
||||||
- ui/tests/lighthouse/**
|
- ui/tests/lighthouse/**
|
||||||
- ui/tests/home/**
|
- ui/tests/home/**
|
||||||
|
- ui/tests/navigation/**
|
||||||
- ui/tests/attack-paths/**
|
- ui/tests/attack-paths/**
|
||||||
|
|
||||||
- name: ui-attack-paths
|
- name: ui-attack-paths
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
api/**
|
api/**
|
||||||
.github/workflows/api-tests.yml
|
.github/workflows/api-tests.yml
|
||||||
|
codecov.yml
|
||||||
files_ignore: |
|
files_ignore: |
|
||||||
api/docs/**
|
api/docs/**
|
||||||
api/README.md
|
api/README.md
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ jobs:
|
|||||||
needs: impact-analysis
|
needs: impact-analysis
|
||||||
if: |
|
if: |
|
||||||
github.repository == 'prowler-cloud/prowler' &&
|
github.repository == 'prowler-cloud/prowler' &&
|
||||||
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) &&
|
||||||
(needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true')
|
(needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
@@ -118,6 +119,104 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }}
|
NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }}
|
||||||
|
|
||||||
|
- name: Validate E2E prerequisites
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
declare -A required=()
|
||||||
|
|
||||||
|
suite_selected() {
|
||||||
|
[[ "${RUN_ALL_TESTS}" == "true" ]] ||
|
||||||
|
[[ " ${E2E_TEST_PATHS} " == *"ui/tests/$1/"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
require_vars() {
|
||||||
|
local variable
|
||||||
|
for variable in "$@"; do
|
||||||
|
required["${variable}"]=1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if suite_selected auth || suite_selected providers ||
|
||||||
|
suite_selected invitations || suite_selected scans ||
|
||||||
|
suite_selected navigation; then
|
||||||
|
require_vars E2E_ADMIN_USER E2E_ADMIN_PASSWORD
|
||||||
|
fi
|
||||||
|
|
||||||
|
if suite_selected sign-up; then
|
||||||
|
require_vars E2E_NEW_USER_PASSWORD
|
||||||
|
fi
|
||||||
|
|
||||||
|
if suite_selected invitations; then
|
||||||
|
require_vars E2E_NEW_USER_PASSWORD E2E_ORGANIZATION_ID
|
||||||
|
fi
|
||||||
|
|
||||||
|
if suite_selected scans; then
|
||||||
|
require_vars \
|
||||||
|
E2E_AWS_PROVIDER_ACCOUNT_ID \
|
||||||
|
E2E_AWS_PROVIDER_ACCESS_KEY \
|
||||||
|
E2E_AWS_PROVIDER_SECRET_KEY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if suite_selected providers; then
|
||||||
|
require_vars \
|
||||||
|
E2E_AWS_PROVIDER_ACCOUNT_ID \
|
||||||
|
E2E_AWS_PROVIDER_ACCESS_KEY \
|
||||||
|
E2E_AWS_PROVIDER_SECRET_KEY \
|
||||||
|
E2E_AWS_PROVIDER_ROLE_ARN \
|
||||||
|
E2E_AZURE_SUBSCRIPTION_ID \
|
||||||
|
E2E_AZURE_CLIENT_ID \
|
||||||
|
E2E_AZURE_SECRET_ID \
|
||||||
|
E2E_AZURE_TENANT_ID \
|
||||||
|
E2E_M365_DOMAIN_ID \
|
||||||
|
E2E_M365_CLIENT_ID \
|
||||||
|
E2E_M365_SECRET_ID \
|
||||||
|
E2E_M365_TENANT_ID \
|
||||||
|
E2E_M365_CERTIFICATE_CONTENT \
|
||||||
|
E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY \
|
||||||
|
E2E_GCP_PROJECT_ID \
|
||||||
|
E2E_GITHUB_APP_ID \
|
||||||
|
E2E_GITHUB_BASE64_APP_PRIVATE_KEY \
|
||||||
|
E2E_GITHUB_USERNAME \
|
||||||
|
E2E_GITHUB_PERSONAL_ACCESS_TOKEN \
|
||||||
|
E2E_GITHUB_ORGANIZATION \
|
||||||
|
E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN \
|
||||||
|
E2E_OCI_TENANCY_ID \
|
||||||
|
E2E_OCI_USER_ID \
|
||||||
|
E2E_OCI_FINGERPRINT \
|
||||||
|
E2E_OCI_KEY_CONTENT \
|
||||||
|
E2E_ALIBABACLOUD_ACCOUNT_ID \
|
||||||
|
E2E_ALIBABACLOUD_ACCESS_KEY_ID \
|
||||||
|
E2E_ALIBABACLOUD_ACCESS_KEY_SECRET \
|
||||||
|
E2E_ALIBABACLOUD_ROLE_ARN \
|
||||||
|
E2E_OKTA_DOMAIN \
|
||||||
|
E2E_OKTA_CLIENT_ID \
|
||||||
|
E2E_OKTA_BASE64_PRIVATE_KEY \
|
||||||
|
E2E_GOOGLEWORKSPACE_CUSTOMER_ID \
|
||||||
|
E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON \
|
||||||
|
E2E_GOOGLEWORKSPACE_DELEGATED_USER \
|
||||||
|
E2E_VERCEL_TEAM_ID \
|
||||||
|
E2E_VERCEL_API_TOKEN
|
||||||
|
fi
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
if (( ${#required[@]} > 0 )); then
|
||||||
|
while IFS= read -r variable; do
|
||||||
|
[[ -z "${!variable:-}" ]] && missing+=("${variable}")
|
||||||
|
done < <(printf '%s\n' "${!required[@]}" | sort)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#missing[@]} > 0 )); then
|
||||||
|
echo "Missing required E2E variables:"
|
||||||
|
printf ' - %s\n' "${missing[@]}"
|
||||||
|
{
|
||||||
|
echo "## Missing E2E prerequisites"
|
||||||
|
printf -- "- \`%s\`\n" "${missing[@]}"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "E2E prerequisite preflight passed."
|
||||||
|
|
||||||
- name: Create k8s Kind Cluster
|
- name: Create k8s Kind Cluster
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1
|
||||||
with:
|
with:
|
||||||
@@ -304,6 +403,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker compose down -v || true
|
docker compose down -v || true
|
||||||
|
|
||||||
|
# Fork pull requests cannot access the secrets required by the E2E suites.
|
||||||
|
fork-e2e-unavailable:
|
||||||
|
needs: impact-analysis
|
||||||
|
if: |
|
||||||
|
github.repository == 'prowler-cloud/prowler' &&
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
github.event.pull_request.head.repo.fork == true &&
|
||||||
|
(needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Report unavailable E2E tests
|
||||||
|
run: |
|
||||||
|
echo "## E2E Tests Skipped" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "UI E2E tests require repository secrets and cannot run for fork pull requests." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
# Skip job - provides clear feedback when no E2E tests needed
|
# Skip job - provides clear feedback when no E2E tests needed
|
||||||
skip-e2e:
|
skip-e2e:
|
||||||
needs: impact-analysis
|
needs: impact-analysis
|
||||||
|
|||||||
@@ -173,3 +173,5 @@ GEMINI.md
|
|||||||
# Docker
|
# Docker
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
docker-compose-dev.override.yml
|
docker-compose-dev.override.yml
|
||||||
|
# Local Pi runtime state
|
||||||
|
.atl/
|
||||||
|
|||||||
@@ -7,5 +7,11 @@ component_management:
|
|||||||
paths:
|
paths:
|
||||||
- "api/**"
|
- "api/**"
|
||||||
|
|
||||||
|
flags:
|
||||||
|
api:
|
||||||
|
paths:
|
||||||
|
- "api/**"
|
||||||
|
carryforward: true
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
layout: "header, diff, flags, components"
|
layout: "header, diff, flags, components"
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).parents[2]
|
||||||
|
WORKFLOW_PATH = REPOSITORY_ROOT / ".github/workflows/api-tests.yml"
|
||||||
|
|
||||||
|
|
||||||
|
def _indented_block(text, heading):
|
||||||
|
lines = text.splitlines()
|
||||||
|
start = lines.index(heading)
|
||||||
|
indentation = len(heading) - len(heading.lstrip())
|
||||||
|
end = len(lines)
|
||||||
|
|
||||||
|
for index in range(start + 1, len(lines)):
|
||||||
|
line = lines[index]
|
||||||
|
if line.strip() and len(line) - len(line.lstrip()) <= indentation:
|
||||||
|
end = index
|
||||||
|
break
|
||||||
|
|
||||||
|
return lines[start:end]
|
||||||
|
|
||||||
|
|
||||||
|
def test_codecov_configuration_changes_run_api_tests():
|
||||||
|
workflow = WORKFLOW_PATH.read_text()
|
||||||
|
changed_files_step = _indented_block(
|
||||||
|
workflow, " - name: Check for API changes"
|
||||||
|
)
|
||||||
|
files = _indented_block("\n".join(changed_files_step), " files: |")
|
||||||
|
|
||||||
|
assert "codecov.yml" in {line.strip() for line in files[1:]}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).parents[2]
|
||||||
|
WORKFLOW_PATH = REPOSITORY_ROOT / ".github/workflows/ui-e2e-tests-v2.yml"
|
||||||
|
NODE_IMAGE_DIGEST = (
|
||||||
|
"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _indented_block(text, heading):
|
||||||
|
lines = text.splitlines()
|
||||||
|
start = lines.index(heading)
|
||||||
|
indentation = len(heading) - len(heading.lstrip())
|
||||||
|
end = len(lines)
|
||||||
|
|
||||||
|
for index in range(start + 1, len(lines)):
|
||||||
|
line = lines[index]
|
||||||
|
if line.strip() and len(line) - len(line.lstrip()) <= indentation:
|
||||||
|
end = index
|
||||||
|
break
|
||||||
|
|
||||||
|
return "\n".join(lines[start:end])
|
||||||
|
|
||||||
|
|
||||||
|
def _multiline_value(block, key):
|
||||||
|
lines = block.splitlines()
|
||||||
|
heading = next(line for line in lines if line.strip() == f"{key}: |")
|
||||||
|
value_block = _indented_block(block, heading)
|
||||||
|
return "\n".join(value_block.splitlines()[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(expression):
|
||||||
|
return " ".join(expression.split())
|
||||||
|
|
||||||
|
|
||||||
|
def test_fork_pull_requests_use_explicit_skip_route():
|
||||||
|
workflow = WORKFLOW_PATH.read_text()
|
||||||
|
e2e_job = _indented_block(workflow, " e2e-tests:")
|
||||||
|
fork_job = _indented_block(workflow, " fork-e2e-unavailable:")
|
||||||
|
|
||||||
|
assert _normalize(_multiline_value(e2e_job, "if")) == (
|
||||||
|
"github.repository == 'prowler-cloud/prowler' && "
|
||||||
|
"(github.event_name != 'pull_request' || "
|
||||||
|
"github.event.pull_request.head.repo.fork == false) && "
|
||||||
|
"(needs.impact-analysis.outputs.has-ui-e2e == 'true' || "
|
||||||
|
"needs.impact-analysis.outputs.run-all == 'true')"
|
||||||
|
)
|
||||||
|
assert _normalize(_multiline_value(fork_job, "if")) == (
|
||||||
|
"github.repository == 'prowler-cloud/prowler' && "
|
||||||
|
"github.event_name == 'pull_request' && "
|
||||||
|
"github.event.pull_request.head.repo.fork == true && "
|
||||||
|
"(needs.impact-analysis.outputs.has-ui-e2e == 'true' || "
|
||||||
|
"needs.impact-analysis.outputs.run-all == 'true')"
|
||||||
|
)
|
||||||
|
assert _indented_block(fork_job, " permissions:") == (
|
||||||
|
" permissions:\n contents: read"
|
||||||
|
)
|
||||||
|
|
||||||
|
reporting_step = _indented_block(
|
||||||
|
fork_job, " - name: Report unavailable E2E tests"
|
||||||
|
)
|
||||||
|
assert "GITHUB_STEP_SUMMARY" in reporting_step
|
||||||
|
assert (
|
||||||
|
"UI E2E tests require repository secrets and cannot run for fork pull requests."
|
||||||
|
in reporting_step
|
||||||
|
)
|
||||||
|
|
||||||
|
prerequisite_step = _indented_block(
|
||||||
|
e2e_job, " - name: Validate E2E prerequisites"
|
||||||
|
)
|
||||||
|
assert "IS_FORK_PR" not in prerequisite_step
|
||||||
|
assert "exit 0" not in prerequisite_step
|
||||||
|
|
||||||
|
|
||||||
|
def test_docker_node_image_matches_nvmrc():
|
||||||
|
node_version = (REPOSITORY_ROOT / "ui/.nvmrc").read_text().strip()
|
||||||
|
dockerfile = (REPOSITORY_ROOT / "ui/Dockerfile").read_text()
|
||||||
|
|
||||||
|
assert f"FROM node:{node_version}-alpine@{NODE_IMAGE_DIGEST} AS base" in dockerfile
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Keep in sync with ui/.nvmrc.
|
# Keep in sync with ui/.nvmrc.
|
||||||
FROM node:24.13.0-alpine@sha256:cd6fb7efa6490f039f3471a189214d5f548c11df1ff9e5b181aa49e22c14383e AS base
|
FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS base
|
||||||
|
|
||||||
LABEL maintainer="https://github.com/prowler-cloud"
|
LABEL maintainer="https://github.com/prowler-cloud"
|
||||||
|
|
||||||
|
|||||||
+40
-57
@@ -129,76 +129,50 @@ export async function addAWSProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the providers page to settle and reports whether the data table is
|
* Finds a provider after the filtered providers page reaches a loaded state.
|
||||||
* present. With zero providers the page renders a full-page empty state
|
* The loading skeleton also renders a table, so table visibility alone cannot
|
||||||
* ("No Providers Configured") instead of the table, so callers must not assume
|
* prove that provider data is ready.
|
||||||
* the table is always there.
|
|
||||||
*/
|
*/
|
||||||
async function providersTableVisibleOrEmptyState(
|
async function findProviderRow(
|
||||||
page: ProvidersPage,
|
page: ProvidersPage,
|
||||||
): Promise<boolean> {
|
providerUID: string,
|
||||||
|
): Promise<Locator | null> {
|
||||||
|
await page.page.goto(
|
||||||
|
`/providers?filter%5Bsearch%5D=${encodeURIComponent(providerUID)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const providerRow = page.providersTable
|
||||||
|
.getByRole("row")
|
||||||
|
.filter({ hasText: providerUID })
|
||||||
|
.first();
|
||||||
|
const noResults = page.providersTable.getByRole("cell", {
|
||||||
|
name: "No results.",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
const emptyState = page.page.getByRole("region", {
|
const emptyState = page.page.getByRole("region", {
|
||||||
name: /no providers configured/i,
|
name: /no providers configured/i,
|
||||||
});
|
});
|
||||||
await expect(page.providersTable.or(emptyState)).toBeVisible({
|
|
||||||
|
await expect(providerRow.or(noResults).or(emptyState)).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
return page.providersTable.isVisible().catch(() => false);
|
|
||||||
|
if (await providerRow.isVisible().catch(() => false)) {
|
||||||
|
return providerRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProviderIfExists(
|
export async function deleteProviderIfExists(
|
||||||
page: ProvidersPage,
|
page: ProvidersPage,
|
||||||
providerUID: string,
|
providerUID: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Delete the provider if it exists
|
|
||||||
|
|
||||||
// Navigate to providers page
|
|
||||||
await page.goto();
|
|
||||||
// With zero providers the page shows the empty state, not the table, so there
|
|
||||||
// is nothing to delete.
|
|
||||||
if (!(await providersTableVisibleOrEmptyState(page))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allRows = page.providersTable.locator("tbody tr");
|
|
||||||
|
|
||||||
const isNoResultsRow = async (row: Locator): Promise<boolean> => {
|
|
||||||
const text = await row.textContent();
|
|
||||||
return text?.includes("No results") || text?.includes("No data") || false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findProviderRow = async (): Promise<Locator | null> => {
|
|
||||||
const rowByText = page.providersTable
|
|
||||||
.locator("tbody tr")
|
|
||||||
.filter({ hasText: providerUID })
|
|
||||||
.first();
|
|
||||||
if (await rowByText.isVisible().catch(() => false)) {
|
|
||||||
return rowByText;
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = await allRows.count();
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const row = allRows.nth(i);
|
|
||||||
if (await isNoResultsRow(row)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const rowText = await row.textContent();
|
|
||||||
if (rowText?.includes(providerUID)) {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the provider row
|
// Find the provider row
|
||||||
const targetRow = await findProviderRow();
|
const targetRow = await findProviderRow(page, providerUID);
|
||||||
|
|
||||||
if (!targetRow) {
|
if (!targetRow) {
|
||||||
// Provider not found, nothing to delete
|
// Provider not found, nothing to delete
|
||||||
// Navigate back to providers page to ensure clean state
|
|
||||||
await page.goto();
|
|
||||||
await providersTableVisibleOrEmptyState(page);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,8 +213,17 @@ export async function deleteProviderIfExists(
|
|||||||
// Wait for modal to close (this indicates deletion was initiated)
|
// Wait for modal to close (this indicates deletion was initiated)
|
||||||
await expect(modal).not.toBeVisible({ timeout: 10000 });
|
await expect(modal).not.toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// Navigate back to providers page to ensure clean state. Deleting the last
|
// The success notification is shown only after the delete request completes.
|
||||||
// provider reveals the empty state instead of an empty table.
|
await expect(
|
||||||
await page.goto();
|
page.page.getByText("The provider was removed successfully.", {
|
||||||
await providersTableVisibleOrEmptyState(page);
|
exact: true,
|
||||||
|
}),
|
||||||
|
).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
// Reload a server-filtered view and prove the provider no longer exists.
|
||||||
|
const deletedProviderRow = await findProviderRow(page, providerUID);
|
||||||
|
expect(
|
||||||
|
deletedProviderRow,
|
||||||
|
`Provider ${providerUID} still exists after deletion`,
|
||||||
|
).toBeNull();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export class HomePage extends BasePage {
|
|||||||
this.dashboardCards = page.locator('[data-testid="dashboard-card"]');
|
this.dashboardCards = page.locator('[data-testid="dashboard-card"]');
|
||||||
this.overviewSection = page.locator('[data-testid="overview-section"]');
|
this.overviewSection = page.locator('[data-testid="overview-section"]');
|
||||||
|
|
||||||
// UI elements
|
// UI elements - the sidebar logo is the home link's accessible content.
|
||||||
this.logo = page.locator('svg[width="300"]');
|
this.logo = page.getByRole("link", { name: "Prowler home" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation methods
|
// Navigation methods
|
||||||
|
|||||||
@@ -107,7 +107,11 @@ export class ScansPage extends BasePage {
|
|||||||
await expect(this.launchScanDialog).toBeVisible();
|
await expect(this.launchScanDialog).toBeVisible();
|
||||||
await this.scanProviderSelect.click();
|
await this.scanProviderSelect.click();
|
||||||
await this.scanProviderSearchInput.fill(uid);
|
await this.scanProviderSearchInput.fill(uid);
|
||||||
await this.scanProviderOption.first().click();
|
|
||||||
|
const providerOption = this.scanProviderOption.first();
|
||||||
|
await expect(providerOption).toBeVisible();
|
||||||
|
await expect(providerOption).toHaveAttribute("aria-disabled", "false");
|
||||||
|
await providerOption.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fillScanNote(note: string): Promise<void> {
|
async fillScanNote(note: string): Promise<void> {
|
||||||
|
|||||||
@@ -67,8 +67,10 @@ export class SignInPage extends BasePage {
|
|||||||
this.signUpLink = page.getByRole("link", { name: "Sign up" });
|
this.signUpLink = page.getByRole("link", { name: "Sign up" });
|
||||||
this.backButton = page.getByRole("button", { name: "Back" });
|
this.backButton = page.getByRole("button", { name: "Back" });
|
||||||
|
|
||||||
// UI elements - title is a <p> element, not a heading
|
// UI elements - ProwlerBrand exposes the deployed brand variant as an image.
|
||||||
this.logo = page.getByRole("img", { name: /Prowler/ });
|
this.logo = page.getByRole("img", {
|
||||||
|
name: /^Prowler (Cloud|Local Server)$/,
|
||||||
|
});
|
||||||
// Use text matching with exact=true to avoid matching other elements
|
// Use text matching with exact=true to avoid matching other elements
|
||||||
this.pageTitle = page.getByText("Welcome back", { exact: true });
|
this.pageTitle = page.getByText("Welcome back", { exact: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user