Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/pr-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ jobs:
needs: changes
if: needs.changes.outputs.protofleet_e2e == 'true'
uses: ./.github/workflows/protofleet-e2e-tests.yml
with:
run_scope: smoke
Comment thread
edgars-avotins marked this conversation as resolved.
secrets: inherit

protoos-e2e-tests:
Expand Down
53 changes: 51 additions & 2 deletions .github/workflows/protofleet-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,29 @@ on:
# Run daily at 6 AM UTC on main branch
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
run_scope:
description: Which Proto Fleet E2E slice to run
required: false
default: all
type: choice
options:
- all
- smoke
workflow_call:
inputs:
run_scope:
description: Which Proto Fleet E2E slice to run
required: false
default: smoke
type: string

jobs:
detect-specs:
runs-on: ubuntu-latest
outputs:
specs: ${{ steps.detect.outputs.specs }}
target_tag: ${{ steps.detect.outputs.target_tag }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -20,16 +36,42 @@ jobs:

- name: Detect spec files
id: detect
env:
RUN_SCOPE: ${{ inputs.run_scope }}
run: |
cd client/e2eTests/protoFleet/spec

if [ -z "$RUN_SCOPE" ]; then
RUN_SCOPE="all"
fi

# Setup specs (NN-*.spec.ts) run automatically as Playwright project
# dependencies in every shard. onboardingVisual.spec.ts runs in its
# own fresh-env job, so only the remaining target specs are sharded.
SPEC_FILES=$( (find . -name "*.spec.ts" | sed 's|^\./||' | sort | grep -Ev '(^[0-9]{2}-.*\.spec\.ts$|^onboardingVisual\.spec\.ts$)' || true) | jq -R . | jq -cs .)
ALL_TARGET_SPECS=$(find . -name "*.spec.ts" | sed 's|^\./||' | sort | grep -Ev '(^[0-9]{2}-.*\.spec\.ts$|^onboardingVisual\.spec\.ts$)' || true)
Comment thread
edgars-avotins marked this conversation as resolved.

if [ "$RUN_SCOPE" = "smoke" ]; then
SPEC_FILES=$(printf '%s\n' "$ALL_TARGET_SPECS" | while IFS= read -r spec; do
if [ -n "$spec" ] && grep -q '@smoke' "$spec"; then
printf '%s\n' "$spec"
fi
done | jq -R . | jq -cs .)
TARGET_TAG="@smoke"
else
SPEC_FILES=$(printf '%s\n' "$ALL_TARGET_SPECS" | jq -R . | jq -cs .)
TARGET_TAG=""
fi

if [ "$RUN_SCOPE" = "smoke" ] && [ "$SPEC_FILES" = "[]" ]; then
echo "No @smoke-tagged ProtoFleet specs were detected."
exit 1
fi

echo "specs=${SPEC_FILES}" >> "$GITHUB_OUTPUT"
echo "target_tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT"
echo "Run scope: ${RUN_SCOPE}"
echo "Detected specs: ${SPEC_FILES}"
echo "Target tag: ${TARGET_TAG:-<none>}"

build:
name: Build client and plugins
Expand Down Expand Up @@ -270,6 +312,7 @@ jobs:
MATRIX_PROJECT: ${{ matrix.project }}
MATRIX_SHARD: ${{ matrix.shard }}
SPECS_JSON: ${{ needs.detect-specs.outputs.specs }}
TARGET_TAG: ${{ needs.detect-specs.outputs.target_tag }}
run: |
rm -rf blob-report playwright-report test-results
mkdir -p blob-report
Expand Down Expand Up @@ -297,9 +340,15 @@ jobs:
echo "Specs for shard ${MATRIX_SHARD}:"
printf ' - %s\n' "${SHARD_SPECS[@]}"

PLAYWRIGHT_ARGS=(--project="$MATRIX_PROJECT" --reporter=blob)
if [ -n "$TARGET_TAG" ]; then
PLAYWRIGHT_ARGS+=(--grep "$TARGET_TAG")
fi
PLAYWRIGHT_ARGS+=("${SHARD_SPECS[@]}")

PLAYWRIGHT_BLOB_OUTPUT_FILE="blob-report/${MATRIX_PROJECT}-shard-${MATRIX_SHARD}.zip" \
PWTEST_BLOB_DO_NOT_REMOVE=1 \
npx playwright test --project="$MATRIX_PROJECT" --reporter=blob "${SHARD_SPECS[@]}"
npx playwright test "${PLAYWRIGHT_ARGS[@]}"

echo "Blob reports created:"
find blob-report -maxdepth 2 -type f -print | sort || true
Expand Down
3 changes: 3 additions & 0 deletions client/e2eTests/protoFleet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ This script:

The Playwright container reaches the host preview through `host.docker.internal`, so this flow is intended for Docker
Desktop on macOS. If port `5173` is already in use, the script automatically falls back to the next free port in the `5173-5193` range. You can still force a specific port with `PREVIEW_PORT=5180`.
If the container ever shows a Vite page saying `Blocked request. This host ("host.docker.internal") is not allowed.`,
the preview server is rejecting the Docker hostname instead of serving Proto Fleet. This repo now allowlists that host
in `vite.config.ts`; if the error returns, verify the refresh flow is still using `vite preview` from this checkout.
The script prefers the Playwright version from `client/node_modules`; if that is unavailable, it falls back to a cached
copy from `~/.npm/_npx` and prints a warning if the version does not match `client/package.json`.

Expand Down
130 changes: 65 additions & 65 deletions client/e2eTests/protoFleet/spec/activity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,47 +42,47 @@ test.describe("Proto Fleet - Activity", () => {
}
});

test("Blink LEDs bulk action is visible in Activity with the right miner count", async ({
activityPage,
commonSteps,
minersPage,
}) => {
await commonSteps.loginAsAdmin();
await commonSteps.goToMinersPage();
test(
"Blink LEDs bulk action is visible in Activity with the right miner count",
{ tag: "@smoke" },
async ({ activityPage, commonSteps, minersPage }) => {
await commonSteps.loginAsAdmin();
await commonSteps.goToMinersPage();

await test.step("Filter to Proto rig miners", async () => {
await minersPage.filterRigMiners();
});
await test.step("Filter to Proto rig miners", async () => {
await minersPage.filterRigMiners();
});

await test.step("Select three miners and trigger Blink LEDs", async () => {
await minersPage.clickMinerCheckboxByIndex(0);
await minersPage.validateActionBarMinerCount(1);
await minersPage.clickMinerCheckboxByIndex(1);
await minersPage.validateActionBarMinerCount(2);
await minersPage.clickMinerCheckboxByIndex(2);
await minersPage.validateActionBarMinerCount(3);
await test.step("Select three miners and trigger Blink LEDs", async () => {
await minersPage.clickMinerCheckboxByIndex(0);
await minersPage.validateActionBarMinerCount(1);
await minersPage.clickMinerCheckboxByIndex(1);
await minersPage.validateActionBarMinerCount(2);
await minersPage.clickMinerCheckboxByIndex(2);
await minersPage.validateActionBarMinerCount(3);

await minersPage.clickBlinkLEDsButton();
});
await minersPage.clickBlinkLEDsButton();
});

await test.step("Validate Blink LEDs toasts", async () => {
await minersPage.validateTextInToastGroup("Blinking LEDs");
await minersPage.validateTextInToastGroup("Blinked LEDs");
});
await test.step("Validate Blink LEDs toasts", async () => {
await minersPage.validateTextInToastGroup("Blinking LEDs");
await minersPage.validateTextInToastGroup("Blinked LEDs");
});

await test.step("Open Activity and filter by user", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.selectUserFilter(testConfig.users.admin.username);
});
await test.step("Open Activity and filter by user", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.selectUserFilter(testConfig.users.admin.username);
});

await test.step("Validate the latest activity row", async () => {
await activityPage.validateLatestActivityDescription("Blinked LEDs");
await activityPage.validateLatestActivityScope("3 miners");
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
await activityPage.validateLatestActivityNotMarkedFailed();
});
});
await test.step("Validate the latest activity row", async () => {
await activityPage.validateLatestActivityDescription("Blinked LEDs");
await activityPage.validateLatestActivityScope("3 miners");
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
await activityPage.validateLatestActivityNotMarkedFailed();
});
},
);

test("Blink LEDs activity detail modal shows batch summary", async ({ activityPage, commonSteps, minersPage }) => {
await test.step("Trigger Blink LEDs for three Proto rig miners", async () => {
Expand Down Expand Up @@ -127,40 +127,40 @@ test.describe("Proto Fleet - Activity", () => {
});
});

test("Type and user filter pills can be removed and Activity export starts a CSV download", async ({
page,
activityPage,
commonSteps,
}) => {
await commonSteps.loginAsAdmin();
test(
"Type and user filter pills can be removed and Activity export starts a CSV download",
{ tag: "@smoke" },
async ({ page, activityPage, commonSteps }) => {
await commonSteps.loginAsAdmin();

await test.step("Open Activity and apply type and user filters", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.selectTypeFilter("Log in");
await activityPage.selectUserFilter(testConfig.users.admin.username);
});
await test.step("Open Activity and apply type and user filters", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.selectTypeFilter("Log in");
await activityPage.selectUserFilter(testConfig.users.admin.username);
});

await test.step("Validate and remove the type filter pill", async () => {
await activityPage.validateFilterPillVisible("Log in");
await activityPage.validateFilterPillVisible(testConfig.users.admin.username);
await activityPage.removeFilterPill("Log in");
await activityPage.validateFilterPillNotVisible("Log in");
await activityPage.validateFilterPillVisible(testConfig.users.admin.username);
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
});
await test.step("Validate and remove the type filter pill", async () => {
await activityPage.validateFilterPillVisible("Log in");
await activityPage.validateFilterPillVisible(testConfig.users.admin.username);
await activityPage.removeFilterPill("Log in");
await activityPage.validateFilterPillNotVisible("Log in");
await activityPage.validateFilterPillVisible(testConfig.users.admin.username);
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
});

await test.step("Export the filtered activity list", async () => {
const download = await activityPage.exportCsv();
test.expect(download.suggestedFilename()).toMatch(/activity-export.*\.csv$/i);
});
await test.step("Export the filtered activity list", async () => {
const download = await activityPage.exportCsv();
test.expect(download.suggestedFilename()).toMatch(/activity-export.*\.csv$/i);
});

await test.step("Keep the list stable after export", async () => {
await page.bringToFront();
await activityPage.waitForActivityListToLoad();
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
});
});
await test.step("Keep the list stable after export", async () => {
await page.bringToFront();
await activityPage.waitForActivityListToLoad();
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
});
},
);

test("Scope filter pills can be removed for group activity", async ({ activityPage, commonSteps, groupsPage }) => {
const groupName = generateRandomText("activity_group");
Expand Down
95 changes: 48 additions & 47 deletions client/e2eTests/protoFleet/spec/activityLogin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,58 @@ test.describe("Proto Fleet - Activity Login", () => {
await page.goto("/");
});

test("Failed login activity is visible after correcting invalid credentials and signing in", async ({
authPage,
activityPage,
}) => {
await test.step("Log in as admin", async () => {
await authPage.inputUsername(testConfig.users.admin.username);
await authPage.inputPassword(testConfig.users.admin.password);
await authPage.clickLogin();
await authPage.validateLoggedIn();
});
test(
"Failed login activity is visible after correcting invalid credentials and signing in",
{ tag: "@smoke" },
async ({ authPage, activityPage }) => {
await test.step("Log in as admin", async () => {
await authPage.inputUsername(testConfig.users.admin.username);
await authPage.inputPassword(testConfig.users.admin.password);
await authPage.clickLogin();
await authPage.validateLoggedIn();
});

await test.step("Confirm the successful login activity is present before testing a failed login", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.selectTypeFilter("Log in");
await activityPage.selectUserFilter(testConfig.users.admin.username);
await activityPage.validateLatestActivityDescription("Logged in");
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
await activityPage.validateLatestActivityNotMarkedFailed();
});
await test.step("Confirm the successful login activity is present before testing a failed login", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.selectTypeFilter("Log in");
await activityPage.selectUserFilter(testConfig.users.admin.username);
await activityPage.validateLatestActivityDescription("Logged in");
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
await activityPage.validateLatestActivityNotMarkedFailed();
});

await test.step("Log out", async () => {
await authPage.logout();
await authPage.validateRedirectedToAuth();
});
await test.step("Log out", async () => {
await authPage.logout();
await authPage.validateRedirectedToAuth();
});

await test.step("Attempt login with an invalid password and validate the error", async () => {
await authPage.inputUsername(testConfig.users.admin.username);
await authPage.inputPassword(`${testConfig.users.admin.password}-invalid`);
await authPage.clickLogin();
await authPage.validateInvalidCredentials();
});
await test.step("Attempt login with an invalid password and validate the error", async () => {
await authPage.inputUsername(testConfig.users.admin.username);
await authPage.inputPassword(`${testConfig.users.admin.password}-invalid`);
await authPage.clickLogin();
await authPage.validateInvalidCredentials();
});

await test.step("Rewrite the correct password and validate the error clears", async () => {
await authPage.inputPassword(testConfig.users.admin.password);
await authPage.validateInvalidCredentialsNotVisible();
});
await test.step("Rewrite the correct password and validate the error clears", async () => {
await authPage.inputPassword(testConfig.users.admin.password);
await authPage.validateInvalidCredentialsNotVisible();
});

await test.step("Log in successfully with corrected credentials", async () => {
await authPage.clickLogin();
await authPage.validateLoggedIn();
});
await test.step("Log in successfully with corrected credentials", async () => {
await authPage.clickLogin();
await authPage.validateLoggedIn();
});

await test.step("Validate the failed login attempt appears in Activity", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.searchActivity("Login failed");
await activityPage.selectUserFilter(testConfig.users.admin.username);
await activityPage.validateLatestActivityDescription("Couldn't log in");
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
await activityPage.validateLatestActivityMarkedFailed();
});
});
await test.step("Validate the failed login attempt appears in Activity", async () => {
await activityPage.navigateToActivityPage();
await activityPage.waitForActivityListToLoad();
await activityPage.searchActivity("Login failed");
await activityPage.selectUserFilter(testConfig.users.admin.username);
await activityPage.validateLatestActivityDescription("Couldn't log in");
await activityPage.validateLatestActivityUser(testConfig.users.admin.username);
await activityPage.validateLatestActivityMarkedFailed();
});
},
);
});
Loading
Loading