mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
test(ui): remove onboarding e2e suite (#11605)
This commit is contained in:
@@ -1,259 +0,0 @@
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
|
||||
import { BasePage } from "../base-page";
|
||||
|
||||
const TOUR_KEY_PREFIX = "prowler.tour";
|
||||
// Keep in sync with addProviderTour.version (lib/tours/add-provider.tour.ts).
|
||||
const ADD_PROVIDER_TOUR_VERSION = 2;
|
||||
const ADD_PROVIDER_TOUR_KEY = `${TOUR_KEY_PREFIX}.add-provider.v${ADD_PROVIDER_TOUR_VERSION}`;
|
||||
const CHECKPOINT_MARKER_KEY = "prowler.onboarding.checkpoint";
|
||||
|
||||
// One popover per active driver.js tour; used to assert single-fire / no auto-fire.
|
||||
const DRIVER_POPOVER_SELECTOR = ".driver-popover";
|
||||
|
||||
// POM for onboarding flows. URL assertions live here; the spec only orchestrates actions.
|
||||
// Note: the sequence store is ephemeral (no persist) — a fresh goto/reload resets it.
|
||||
export class OnboardingPage extends BasePage {
|
||||
readonly welcomeModal: Locator;
|
||||
readonly getStartedButton: Locator;
|
||||
readonly skipButton: Locator;
|
||||
|
||||
readonly checkpointDialog: Locator;
|
||||
readonly checkpointContinueButton: Locator;
|
||||
readonly checkpointFinishButton: Locator;
|
||||
|
||||
readonly addProviderTriggerAnchor: Locator;
|
||||
readonly providerTypeAnchor: Locator;
|
||||
|
||||
readonly viewFirstScanLaunchAnchor: Locator;
|
||||
readonly viewFirstScanTabsAnchor: Locator;
|
||||
readonly exploreFindingsFiltersAnchor: Locator;
|
||||
readonly exploreFindingsGroupAnchor: Locator;
|
||||
readonly exploreFindingsResourcesAnchor: Locator;
|
||||
readonly viewComplianceFrameworksAnchor: Locator;
|
||||
readonly viewComplianceSearchAnchor: Locator;
|
||||
readonly attackPathsIntroAnchor: Locator;
|
||||
readonly attackPathsScanListAnchor: Locator;
|
||||
|
||||
readonly accountMenuTrigger: Locator;
|
||||
readonly productTourSubTrigger: Locator;
|
||||
|
||||
readonly driverPopover: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
|
||||
this.welcomeModal = page.getByRole("dialog").filter({
|
||||
has: page.getByRole("heading", { name: /Add your first provider/i }),
|
||||
});
|
||||
this.getStartedButton = page.getByRole("button", { name: "Get started" });
|
||||
this.skipButton = page.getByRole("button", { name: "Skip for now" });
|
||||
|
||||
this.checkpointDialog = page.getByRole("dialog").filter({
|
||||
has: page.getByRole("heading", {
|
||||
name: /Provider added — keep exploring\?/i,
|
||||
}),
|
||||
});
|
||||
this.checkpointContinueButton = page.getByRole("button", {
|
||||
name: "Continue the tour",
|
||||
});
|
||||
this.checkpointFinishButton = page.getByRole("button", {
|
||||
name: "Finish here",
|
||||
});
|
||||
|
||||
this.addProviderTriggerAnchor = page.locator(
|
||||
'[data-tour-id="add-provider-trigger"]',
|
||||
);
|
||||
this.providerTypeAnchor = page.locator(
|
||||
'[data-tour-id="add-provider-provider-type"]',
|
||||
);
|
||||
|
||||
this.viewFirstScanLaunchAnchor = page.locator(
|
||||
'[data-tour-id="view-first-scan-launch"]',
|
||||
);
|
||||
this.viewFirstScanTabsAnchor = page.locator(
|
||||
'[data-tour-id="view-first-scan-tabs"]',
|
||||
);
|
||||
this.exploreFindingsFiltersAnchor = page.locator(
|
||||
'[data-tour-id="explore-findings-filters"]',
|
||||
);
|
||||
this.exploreFindingsGroupAnchor = page.locator(
|
||||
'[data-tour-id="explore-findings-group"]',
|
||||
);
|
||||
this.exploreFindingsResourcesAnchor = page.locator(
|
||||
'[data-tour-id="explore-findings-resources"]',
|
||||
);
|
||||
this.viewComplianceFrameworksAnchor = page.locator(
|
||||
'[data-tour-id="view-compliance-frameworks"]',
|
||||
);
|
||||
this.viewComplianceSearchAnchor = page.locator(
|
||||
'[data-tour-id="view-compliance-search"]',
|
||||
);
|
||||
this.attackPathsIntroAnchor = page.locator(
|
||||
'[data-tour-id="attack-paths-intro"]',
|
||||
);
|
||||
this.attackPathsScanListAnchor = page.locator(
|
||||
'[data-tour-id="attack-paths-scan-list"]',
|
||||
);
|
||||
|
||||
this.accountMenuTrigger = page.getByRole("button", {
|
||||
name: "Account menu",
|
||||
});
|
||||
this.productTourSubTrigger = page.getByRole("menuitem", {
|
||||
name: "Product tour",
|
||||
});
|
||||
|
||||
this.driverPopover = page.locator(DRIVER_POPOVER_SELECTOR);
|
||||
}
|
||||
|
||||
// Clears all tour records and the checkpoint marker so the gate evaluates as a fresh browser.
|
||||
async clearOnboardingState(): Promise<void> {
|
||||
await this.page.evaluate(
|
||||
({ prefix, checkpointKey }) => {
|
||||
const keys: string[] = [];
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i);
|
||||
if (key && key.startsWith(prefix)) keys.push(key);
|
||||
}
|
||||
keys.forEach((key) => window.localStorage.removeItem(key));
|
||||
window.localStorage.removeItem(checkpointKey);
|
||||
},
|
||||
{ prefix: TOUR_KEY_PREFIX, checkpointKey: CHECKPOINT_MARKER_KEY },
|
||||
);
|
||||
}
|
||||
|
||||
// Seeds a completed record so the restart path can prove the tour re-fires anyway.
|
||||
async seedCompletedAddProviderRecord(): Promise<void> {
|
||||
await this.page.evaluate(
|
||||
({ key, version }) => {
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
tourId: "add-provider",
|
||||
version,
|
||||
state: "completed",
|
||||
completedAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ key: ADD_PROVIDER_TOUR_KEY, version: ADD_PROVIDER_TOUR_VERSION },
|
||||
);
|
||||
}
|
||||
|
||||
async openAccountMenu(): Promise<void> {
|
||||
await this.accountMenuTrigger.click();
|
||||
}
|
||||
|
||||
// Hover the sub-trigger to open the per-flow submenu.
|
||||
async openProductTourSubmenu(): Promise<void> {
|
||||
await this.openAccountMenu();
|
||||
await expect(this.productTourSubTrigger).toBeVisible();
|
||||
await this.productTourSubTrigger.hover();
|
||||
}
|
||||
|
||||
tourFlowMenuItem(title: string): Locator {
|
||||
return this.page.getByRole("menuitem", { name: title, exact: true });
|
||||
}
|
||||
|
||||
async selectTourFlow(title: string): Promise<void> {
|
||||
await this.openProductTourSubmenu();
|
||||
const item = this.tourFlowMenuItem(title);
|
||||
await expect(item).toBeVisible();
|
||||
await item.click();
|
||||
}
|
||||
|
||||
async clickGetStarted(): Promise<void> {
|
||||
await this.getStartedButton.click();
|
||||
}
|
||||
|
||||
async clickCheckpointContinue(): Promise<void> {
|
||||
await this.checkpointContinueButton.click();
|
||||
}
|
||||
|
||||
async clickCheckpointFinish(): Promise<void> {
|
||||
await this.checkpointFinishButton.click();
|
||||
}
|
||||
|
||||
async closeActiveTour(): Promise<void> {
|
||||
await this.page.keyboard.press("Escape");
|
||||
}
|
||||
|
||||
async verifyWelcomeModalVisible(): Promise<void> {
|
||||
await expect(this.welcomeModal).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyWelcomeModalNotVisible(): Promise<void> {
|
||||
await expect(this.welcomeModal).not.toBeVisible();
|
||||
}
|
||||
|
||||
async verifyCheckpointDialogVisible(): Promise<void> {
|
||||
await expect(this.checkpointDialog).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyTriggerAnchorPresent(): Promise<void> {
|
||||
await expect(this.addProviderTriggerAnchor).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyAnchorsPresent(): Promise<void> {
|
||||
await expect(this.addProviderTriggerAnchor).toBeVisible();
|
||||
await expect(this.providerTypeAnchor).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyViewFirstScanAnchorPresent(): Promise<void> {
|
||||
await expect(this.viewFirstScanLaunchAnchor).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyExploreFindingsAnchorPresent(): Promise<void> {
|
||||
await expect(this.exploreFindingsFiltersAnchor).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyViewComplianceAnchorPresent(): Promise<void> {
|
||||
await expect(this.viewComplianceFrameworksAnchor).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyAttackPathsAnchorPresent(): Promise<void> {
|
||||
await expect(this.attackPathsIntroAnchor).toBeVisible();
|
||||
}
|
||||
|
||||
// OB-E2E-006: page owns the driver; onboarding must not mount a second one.
|
||||
async verifySingleDriverPopover(): Promise<void> {
|
||||
await expect(this.driverPopover).toHaveCount(1);
|
||||
}
|
||||
|
||||
// OB-E2E-007/004: no tour auto-fires after a reload or stop.
|
||||
async verifyNoDriverPopover(): Promise<void> {
|
||||
await expect(this.driverPopover).toHaveCount(0);
|
||||
}
|
||||
|
||||
async verifyOnProvidersPage(): Promise<void> {
|
||||
await this.page.waitForURL(/\/providers(\?|$)/);
|
||||
}
|
||||
|
||||
async verifyOnProvidersPageWithOnboardingParam(): Promise<void> {
|
||||
await this.page.waitForURL(/\/providers\?onboarding=add-provider/);
|
||||
}
|
||||
|
||||
async verifyOnScansPage(): Promise<void> {
|
||||
await this.page.waitForURL(/\/scans(\?|$)/);
|
||||
}
|
||||
|
||||
async verifyOnFindingsPage(): Promise<void> {
|
||||
await this.page.waitForURL(/\/findings(\?|$)/);
|
||||
}
|
||||
|
||||
async verifyOnFindingsReplayPage(): Promise<void> {
|
||||
await this.page.waitForURL(/\/findings\?onboarding=explore-findings/);
|
||||
}
|
||||
|
||||
async verifyOnCompliancePage(): Promise<void> {
|
||||
await this.page.waitForURL(/\/compliance(\?|$)/);
|
||||
}
|
||||
|
||||
async verifyOnAttackPathsPage(): Promise<void> {
|
||||
await this.page.waitForURL(/\/attack-paths/);
|
||||
}
|
||||
|
||||
async verifyStillOnFindingsPage(): Promise<void> {
|
||||
await expect(this.page).toHaveURL(/\/findings/);
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
### E2E Tests: Onboarding System
|
||||
|
||||
**Suite ID:** `OB-E2E`
|
||||
**Feature:** Extensible UI onboarding system: the guided add-provider tour, the
|
||||
cross-route guided sequence after the first provider connects, and per-flow
|
||||
manual replay from the avatar menu.
|
||||
|
||||
> Behavioral assertions only: Welcome modal visibility, checkpoint dialog
|
||||
> visibility, navigation to each flow route, presence of the `data-tour-id`
|
||||
> anchors in the DOM, and localStorage markers. Driver.js overlay animation is
|
||||
> never asserted. Waits use `expect(...).toBeVisible()` and
|
||||
> `page.waitForURL()` — never `networkidle`.
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-001` - Mandatory new-user onboarding path
|
||||
|
||||
**Priority:** `critical`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** A zero-provider authenticated user is forced into the
|
||||
Welcome modal on first load; accepting it navigates to the add-provider flow and
|
||||
exposes the tour trigger anchor.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (`admin.auth.setup`, reused `storageState`)
|
||||
- All `prowler.tour.*` localStorage keys cleared before the test
|
||||
- The account has zero providers (existing providers removed via `deleteProviderIfExists`)
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Navigate to a page inside `app/(prowler)/`
|
||||
2. Assert the Welcome modal is visible
|
||||
3. Click "Get started"
|
||||
4. Assert navigation to `/providers` (the `?onboarding=add-provider` param is consumed)
|
||||
5. Assert the `data-tour-id="add-provider-trigger"` anchor is present in the DOM
|
||||
|
||||
### Expected Result
|
||||
|
||||
- Welcome modal is displayed for the zero-provider user
|
||||
- Accepting navigates to the providers route
|
||||
- The add-provider trigger anchor is mounted on the providers page
|
||||
|
||||
### Key verification points
|
||||
|
||||
- Welcome modal visible on first authenticated load (gate forced it)
|
||||
- After accept, the URL is `/providers`
|
||||
- `[data-tour-id="add-provider-trigger"]` exists in the DOM
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec: "Zero-provider user on first authenticated load", "User accepts
|
||||
the Welcome modal", "Every tour step target has a matching data-tour-id anchor"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env). The trigger
|
||||
anchor proves the tour surface is reachable without asserting overlay animation
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-002` - Restart onboarding from the avatar menu
|
||||
|
||||
**Priority:** `high`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** A user who already completed the tour can restart it
|
||||
from the avatar menu; the tour starts again despite the existing completion
|
||||
record.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (`admin.auth.setup`, reused `storageState`)
|
||||
- A `completed` record exists in localStorage for `add-provider`
|
||||
(`prowler.tour.add-provider.v2`)
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Navigate to a page with the user nav visible
|
||||
2. Open the avatar account menu
|
||||
3. Click "Product tour"
|
||||
4. Assert navigation to `/providers?onboarding=add-provider`
|
||||
5. Assert the `data-tour-id="add-provider-trigger"` anchor is present in the DOM
|
||||
|
||||
### Expected Result
|
||||
|
||||
- The restart entry navigates to the add-provider flow route with the onboarding param
|
||||
- The tour starts despite the existing completion record (no Welcome modal)
|
||||
- The add-provider trigger anchor is mounted on the providers page
|
||||
|
||||
### Key verification points
|
||||
|
||||
- "Product tour" entry is present in the avatar menu
|
||||
- After selecting it, the URL is `/providers?onboarding=add-provider`
|
||||
- `[data-tour-id="add-provider-trigger"]` exists in the DOM (re-trigger bypassed the completion record)
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec: "User activates the restart entry point from the avatar menu",
|
||||
"Re-trigger works after a full page reload", "Re-trigger does not depend on
|
||||
prior browser state"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env)
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-003` - First-run guided sequence + checkpoint
|
||||
|
||||
**Priority:** `critical`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** After the first provider connects, the checkpoint
|
||||
dialog offers a guided sequence; "Continue the tour" chains through scans,
|
||||
findings, compliance, and attack paths, advancing only on tour completion.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (`admin.auth.setup`, reused `storageState`)
|
||||
- All `prowler.tour.*` keys and the `prowler.onboarding.checkpoint` marker cleared
|
||||
- A connected provider exists (a `false → true` `hasProviders` flip is reachable);
|
||||
guarded/skipped when `E2E_AWS_PROVIDER_ACCOUNT_ID` is unset
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Start zero-provider; assert the Welcome modal is visible
|
||||
2. Connect a provider (real flip via `addAWSProvider`)
|
||||
3. Assert the checkpoint dialog "Provider added — keep exploring?" is visible
|
||||
4. Click "Continue the tour"
|
||||
5. Assert `/scans` with `data-tour-id="view-first-scan-launch"` present
|
||||
6. Complete the scans tour; assert `/findings` with `explore-findings-filters`
|
||||
7. Complete; assert `/compliance` with `view-compliance-frameworks`
|
||||
8. Complete; assert `/attack-paths` with `attack-paths-intro` present
|
||||
|
||||
### Expected Result
|
||||
|
||||
- The checkpoint fires once on the real provider-connected flip
|
||||
- Continuing chains through each flow route in registry order
|
||||
- Each route exposes its first anchor in the DOM
|
||||
|
||||
### Key verification points
|
||||
|
||||
- Checkpoint dialog visible after the provider connects
|
||||
- Sequence visits `/scans → /findings → /compliance → /attack-paths`
|
||||
- The route-specific anchor is present at each step
|
||||
- `prowler.onboarding.checkpoint` marker is set after a choice
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec `onboarding-sequence`: "Checkpoint after first provider connects",
|
||||
"Continue starts the sequence", "Next flow starts after navigating to its route"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env)
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-004` - Stop the sequence at any time
|
||||
|
||||
**Priority:** `high`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** Closing any tour mid-sequence ends the sequence; no
|
||||
further flow auto-fires and a reload does not resume it.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (reused `storageState`)
|
||||
- A connected provider exists; guarded/skipped when `E2E_AWS_PROVIDER_ACCOUNT_ID` is unset
|
||||
- Tour state and checkpoint marker cleared in `beforeEach`
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Start the guided sequence (continue from the checkpoint)
|
||||
2. On `/findings`, close the active tour (press Escape)
|
||||
3. Wait briefly and assert the URL is still `/findings` (no advance to `/compliance`)
|
||||
4. Reload the page
|
||||
5. Assert no tour auto-fires (no `.driver-popover` in the DOM)
|
||||
|
||||
### Expected Result
|
||||
|
||||
- Closing the tour stops the sequence immediately
|
||||
- No navigation to `/compliance` occurs
|
||||
- A reload does not resume the sequence
|
||||
|
||||
### Key verification points
|
||||
|
||||
- URL remains `/findings` after Escape (no auto-advance)
|
||||
- After reload, zero `.driver-popover` elements exist
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec `onboarding-sequence`: "Stopping any tour ends the sequence"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env)
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-005` - Manual single-flow replay from the avatar menu
|
||||
|
||||
**Priority:** `high`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** The avatar "Product tour" submenu lists every flow;
|
||||
selecting one replays that single flow only and never chains into the sequence.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (reused `storageState`)
|
||||
- `completed` records seeded for the flows so the list represents replay state
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Open the avatar account menu
|
||||
2. Open the "Product tour" submenu
|
||||
3. Assert all five flow titles are listed (registry order)
|
||||
4. Select "Explore your findings"
|
||||
5. Assert navigation to `/findings?onboarding=explore-findings`
|
||||
6. Assert `data-tour-id="explore-findings-filters"` present
|
||||
7. Close the tour and assert no navigation to `/compliance` (no sequence chaining)
|
||||
|
||||
### Expected Result
|
||||
|
||||
- The submenu lists all five flows by title
|
||||
- Selecting a flow replays it standalone with the `?onboarding=<id>` param
|
||||
- Closing the replayed tour does not advance to the next flow
|
||||
|
||||
### Key verification points
|
||||
|
||||
- Submenu contains `Add your first provider`, `Run your first scan`,
|
||||
`Explore your findings`, `Check compliance`, `Visualize attack paths`
|
||||
- URL is `/findings?onboarding=explore-findings`
|
||||
- No advance to `/compliance` after closing
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec `onboarding` (MODIFIED): "Avatar entry opens an ordered list of
|
||||
flows", "Selecting a flow replays that single flow only"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env)
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-006` - Attack-paths single-fire
|
||||
|
||||
**Priority:** `high`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** On `/attack-paths` only one driver popover exists at a
|
||||
time — the page owns the driver and onboarding never mounts a second runner.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (reused `storageState`)
|
||||
- A connected provider with at least one ready scan exists; guarded/skipped when
|
||||
`E2E_AWS_PROVIDER_ACCOUNT_ID` is unset
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Navigate to `/attack-paths?onboarding=attack-paths`
|
||||
2. Wait for the attack-paths tour popover to appear
|
||||
3. Count `.driver-popover` elements in the DOM
|
||||
|
||||
### Expected Result
|
||||
|
||||
- Exactly one driver popover exists (no double-fire)
|
||||
|
||||
### Key verification points
|
||||
|
||||
- `.driver-popover` count is exactly `1`
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec `onboarding-sequence`: "Attack-Paths Single-Fire Integration"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env)
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `OB-E2E-007` - Refresh mid-sequence does not re-fire
|
||||
|
||||
**Priority:** `high`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e
|
||||
- feature → @onboarding
|
||||
|
||||
**Description/Objective:** The sequence slice is ephemeral; a hard reload mid-tour
|
||||
resets it so no tour auto-fires, and no Welcome modal appears (provider connected).
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (reused `storageState`)
|
||||
- A connected provider exists; guarded/skipped when `E2E_AWS_PROVIDER_ACCOUNT_ID` is unset
|
||||
- Tour state and checkpoint marker cleared in `beforeEach`
|
||||
|
||||
### Flow Steps
|
||||
|
||||
1. Start the guided sequence and land on `/scans`
|
||||
2. Hard-reload the page
|
||||
3. Assert no `.driver-popover` auto-fires
|
||||
4. Assert the Welcome modal is not visible
|
||||
|
||||
### Expected Result
|
||||
|
||||
- No tour auto-fires after the reload (ephemeral slice reset)
|
||||
- No Welcome modal (the provider is connected, so the gate stays closed)
|
||||
|
||||
### Key verification points
|
||||
|
||||
- Zero `.driver-popover` elements after reload
|
||||
- Welcome modal is not visible
|
||||
|
||||
### Notes
|
||||
|
||||
- Maps to spec `onboarding-sequence`: "Refresh mid-sequence does not re-fire
|
||||
infinitely"
|
||||
- Full execution requires the Prowler stack (API + DB + auth env)
|
||||
@@ -1,245 +0,0 @@
|
||||
import { test } from "@playwright/test";
|
||||
|
||||
import { addAWSProvider, deleteProviderIfExists } from "../helpers";
|
||||
import { ProvidersPage } from "../providers/providers-page";
|
||||
import { OnboardingPage } from "./onboarding-page";
|
||||
|
||||
// Real AWS credentials for the hasProviders false→true flip; tests skip when unset.
|
||||
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID ?? "";
|
||||
const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY ?? "";
|
||||
const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY ?? "";
|
||||
const hasAwsCredentials = Boolean(accountId && accessKey && secretKey);
|
||||
|
||||
// Guided onboarding is a Prowler Cloud-only feature; it never mounts in OSS.
|
||||
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
|
||||
test.describe("Onboarding", () => {
|
||||
test.skip(
|
||||
!isCloudEnv,
|
||||
"Guided onboarding is a Prowler Cloud-only feature (NEXT_PUBLIC_IS_CLOUD_ENV != true)",
|
||||
);
|
||||
test.use({ storageState: "playwright/.auth/admin_user.json" });
|
||||
|
||||
test.describe("Mandatory new-user path", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const providersPage = new ProvidersPage(page);
|
||||
if (accountId) {
|
||||
await deleteProviderIfExists(providersPage, accountId);
|
||||
}
|
||||
|
||||
await providersPage.goto();
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
await onboardingPage.clearOnboardingState();
|
||||
});
|
||||
|
||||
test(
|
||||
"forces the Welcome modal and hands off to the add-provider tour",
|
||||
{
|
||||
tag: ["@critical", "@e2e", "@onboarding", "@OB-E2E-001"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
!accountId,
|
||||
"E2E_AWS_PROVIDER_ACCOUNT_ID is not set; cannot guarantee a zero-provider account",
|
||||
);
|
||||
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/providers");
|
||||
await onboardingPage.verifyWelcomeModalVisible();
|
||||
await onboardingPage.clickGetStarted();
|
||||
await onboardingPage.verifyOnProvidersPage();
|
||||
await onboardingPage.verifyTriggerAnchorPresent();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Restart path", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
await onboardingPage.goto("/providers");
|
||||
await onboardingPage.seedCompletedAddProviderRecord();
|
||||
});
|
||||
|
||||
test(
|
||||
"restarts the tour from the avatar menu despite a completion record",
|
||||
{
|
||||
tag: ["@high", "@e2e", "@onboarding", "@OB-E2E-002"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/providers");
|
||||
await onboardingPage.selectTourFlow("Add your first provider");
|
||||
await onboardingPage.verifyOnProvidersPageWithOnboardingParam();
|
||||
await onboardingPage.verifyTriggerAnchorPresent();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Guided sequence", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const providersPage = new ProvidersPage(page);
|
||||
if (accountId) {
|
||||
await deleteProviderIfExists(providersPage, accountId);
|
||||
}
|
||||
await providersPage.goto();
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
await onboardingPage.clearOnboardingState();
|
||||
});
|
||||
|
||||
test(
|
||||
"runs the first-run sequence after the checkpoint and chains every flow",
|
||||
{
|
||||
tag: ["@critical", "@e2e", "@onboarding", "@OB-E2E-003"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
!hasAwsCredentials,
|
||||
"E2E AWS provider credentials are not set; cannot drive a real hasProviders flip",
|
||||
);
|
||||
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/providers");
|
||||
await onboardingPage.verifyWelcomeModalVisible();
|
||||
|
||||
// Connect a provider to drive the genuine hasProviders false→true flip.
|
||||
await addAWSProvider(page, accountId, accessKey, secretKey);
|
||||
|
||||
await onboardingPage.verifyCheckpointDialogVisible();
|
||||
await onboardingPage.clickCheckpointContinue();
|
||||
|
||||
await onboardingPage.verifyOnScansPage();
|
||||
await onboardingPage.verifyViewFirstScanAnchorPresent();
|
||||
await onboardingPage.closeActiveTour();
|
||||
|
||||
await onboardingPage.verifyExploreFindingsAnchorPresent();
|
||||
|
||||
await onboardingPage.goto("/compliance");
|
||||
await onboardingPage.verifyViewComplianceAnchorPresent();
|
||||
await onboardingPage.goto("/attack-paths");
|
||||
await onboardingPage.verifyAttackPathsAnchorPresent();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"stops the sequence when a tour is closed and does not resume on reload",
|
||||
{
|
||||
tag: ["@high", "@e2e", "@onboarding", "@OB-E2E-004"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
!hasAwsCredentials,
|
||||
"E2E AWS provider credentials are not set; cannot drive the guided sequence",
|
||||
);
|
||||
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/providers");
|
||||
await addAWSProvider(page, accountId, accessKey, secretKey);
|
||||
await onboardingPage.verifyCheckpointDialogVisible();
|
||||
await onboardingPage.clickCheckpointContinue();
|
||||
|
||||
await onboardingPage.verifyOnScansPage();
|
||||
await onboardingPage.closeActiveTour();
|
||||
await onboardingPage.verifyExploreFindingsAnchorPresent();
|
||||
await onboardingPage.closeActiveTour();
|
||||
|
||||
// Closing the tour stops the sequence — no auto-advance to compliance.
|
||||
await onboardingPage.verifyStillOnFindingsPage();
|
||||
|
||||
// Ephemeral sequence must not resume after a hard reload.
|
||||
await onboardingPage.refresh();
|
||||
await onboardingPage.verifyNoDriverPopover();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"does not re-fire after a hard reload mid-sequence",
|
||||
{
|
||||
tag: ["@high", "@e2e", "@onboarding", "@OB-E2E-007"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
!hasAwsCredentials,
|
||||
"E2E AWS provider credentials are not set; cannot drive the guided sequence",
|
||||
);
|
||||
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/providers");
|
||||
await addAWSProvider(page, accountId, accessKey, secretKey);
|
||||
await onboardingPage.verifyCheckpointDialogVisible();
|
||||
await onboardingPage.clickCheckpointContinue();
|
||||
await onboardingPage.verifyOnScansPage();
|
||||
|
||||
// Hard reload resets the ephemeral slice; provider is connected so the gate stays silent.
|
||||
await onboardingPage.refresh();
|
||||
await onboardingPage.verifyNoDriverPopover();
|
||||
await onboardingPage.verifyWelcomeModalNotVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Manual replay", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
await onboardingPage.goto("/providers");
|
||||
await onboardingPage.seedCompletedAddProviderRecord();
|
||||
});
|
||||
|
||||
test(
|
||||
"replays a single flow from the avatar submenu without chaining",
|
||||
{
|
||||
tag: ["@high", "@e2e", "@onboarding", "@OB-E2E-005"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/providers");
|
||||
await onboardingPage.openProductTourSubmenu();
|
||||
for (const title of [
|
||||
"Add your first provider",
|
||||
"Run your first scan",
|
||||
"Explore your findings",
|
||||
"Check compliance",
|
||||
"Visualize attack paths",
|
||||
]) {
|
||||
await onboardingPage.verifyElementVisible(
|
||||
onboardingPage.tourFlowMenuItem(title),
|
||||
);
|
||||
}
|
||||
|
||||
await onboardingPage.tourFlowMenuItem("Explore your findings").click();
|
||||
await onboardingPage.verifyOnFindingsReplayPage();
|
||||
await onboardingPage.verifyExploreFindingsAnchorPresent();
|
||||
|
||||
// Replay must not chain to the next flow on close.
|
||||
await onboardingPage.closeActiveTour();
|
||||
await onboardingPage.verifyStillOnFindingsPage();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Attack-paths single-fire", () => {
|
||||
test(
|
||||
"renders exactly one driver popover on the attack-paths route",
|
||||
{
|
||||
tag: ["@high", "@e2e", "@onboarding", "@OB-E2E-006"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
!hasAwsCredentials,
|
||||
"E2E AWS provider credentials are not set; attack-paths needs a ready scan",
|
||||
);
|
||||
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await onboardingPage.goto("/attack-paths?onboarding=attack-paths");
|
||||
await onboardingPage.verifyOnAttackPathsPage();
|
||||
await onboardingPage.verifySingleDriverPopover();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user