mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
## What Gate the full **Playwright (UI)** and **API** test suites on PRs behind a single **`e2e` label**, and **auto-bypass** PRs that only touch files which can't affect those suites. Covers `playwright.yml` and `api-testing.yml`. ## Behaviour (both suites) | Situation | build/test jobs | summary check (`playwright_summary` / `api_tests_summary`) | Mergeable? | |---|---|---|---| | PR with `e2e` label | full suite runs | green if tests pass | ✅ if green | | PR, no label, relevant code changed | skipped | **fails** ("add `e2e` label") | ❌ blocked | | PR, no label, docs/meta only | skipped | **passes** (auto-bypass) | ✅ | | Remove the `e2e` label | skipped (fresh run) | **fails** again | ❌ until re-added | | push to `main` / merge queue / dispatch | full suite runs | green if tests pass | n/a | One `e2e` label runs **both** UI + API. `push`, `merge_group`, and `workflow_dispatch` are never label-gated. ## Why the gate lives in the summary job A job that `if:`-skips reports `skipped`, which branch protection treats as **pass**. So a no-label PR can't be blocked by merely skipping jobs — the summary job **actively fails** when the change is relevant and the `e2e` label is absent. ## Auto-bypass (`check_changes`) No label needed for: `*.md/.mdx/.markdown/.rst/.adoc`, `docs/`, images/media, `LICENSE/NOTICE/AUTHORS/CONTRIBUTORS/CHANGELOG/.mailmap`, `CODEOWNERS`, GitHub issue/PR/funding templates, `dependabot`/`renovate`, `.gitignore/.gitattributes/.editorconfig/.gitkeep/.husky/.vscode/.idea`. The **API** workflow additionally bypasses `web/` + UI-test dirs (frontend changes don't affect API tests). **Not** bypassed (still trigger): `.github/workflows/**` (incl. these workflow files), source, `package*.json`/lockfiles, configs, `*.txt`. ## Rollout prerequisites - Create the **`e2e`** label in this repo. - Required status checks = **`playwright_summary`** and **`api_tests_summary`** — the individual shard jobs must NOT be required (they legitimately skip and would auto-pass). Sibling PR on the other repo uses the same branch `ci/e2e-label-gate`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
285 lines
13 KiB
JavaScript
285 lines
13 KiB
JavaScript
// Copyright 2026 OpenObserve Inc.
|
|
|
|
import { expect } from '@playwright/test';
|
|
import { openOSelectDropdown } from '../alertsPages/oselectHelpers.js';
|
|
|
|
export class CipherKeysFormValidationPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// Navigation
|
|
this.settingsMenu = page.locator('[data-test="menu-link-/settings-item"]');
|
|
this.cipherKeyTab = page.locator('[data-test="management-cipher-key-tab"]');
|
|
this.addCipherKeyButton = page.locator('[data-test="cipher-keys-add-btn"]');
|
|
|
|
// AddCipherKey — name field
|
|
this.nameInput = page.locator('[data-test="add-cipher-key-name-input-field"]');
|
|
this.nameError = page.locator('[data-test="add-cipher-key-name-input-error"]');
|
|
|
|
// AddCipherKey — store type select
|
|
this.storeTypeSelect = page.locator('[data-test="add-cipher-key-type-input"]');
|
|
this.storeTypeError = page.locator('[data-test="add-cipher-key-type-input-error"]');
|
|
this.storeTypeAkeylessOption = page.locator('[data-test="add-cipher-key-type-input-option"][data-test-value="akeyless"]');
|
|
|
|
// AddCipherKey — step buttons
|
|
this.continueButton = page.locator('[data-test="add-report-step1-continue-btn"]');
|
|
this.saveButton = page.locator('[data-test="add-cipher-key-save-btn"]');
|
|
this.cancelButton = page.locator('[data-test="add-cipher-key-cancel-btn"]');
|
|
this.backButton = page.locator('[data-test="add-cipher-key-step2-back-btn"]');
|
|
|
|
// AddOpenobserveType — secret textarea
|
|
this.secretInput = page.locator('[data-test="add-cipher-key-openobserve-secret-input-field"]');
|
|
this.secretError = page.locator('[data-test="add-cipher-key-openobserve-secret-input-error"]');
|
|
|
|
// AddAkeylessType — base URL
|
|
this.akeylessBaseUrlInput = page.locator('[data-test="add-cipher-key-akeyless-baseurl-input-field"]');
|
|
this.akeylessBaseUrlError = page.locator('[data-test="add-cipher-key-akeyless-baseurl-input-error"]');
|
|
|
|
// AddAkeylessType — access ID
|
|
this.akeylessAccessIdInput = page.locator('[data-test="add-cipher-key-akeyless-access-id-input-field"]');
|
|
this.akeylessAccessIdError = page.locator('[data-test="add-cipher-key-akeyless-access-id-input-error"]');
|
|
|
|
// AddAkeylessType — auth method select (akeyless step 1 sub-form).
|
|
// NOTE: shares the data-test "add-cipher-key-auth-method-input" with the
|
|
// AddEncryptionMechanism provider-type select, but the two are never mounted
|
|
// at the same time (akeyless = step 1 sub-form, mechanism = step 2).
|
|
this.authMethodSelect = page.locator('[data-test="add-cipher-key-auth-method-input"]');
|
|
this.authMethodError = page.locator('[data-test="add-cipher-key-auth-method-input-error"]');
|
|
|
|
// AddAkeylessType — access key
|
|
this.akeylessAccessKeyInput = page.locator('[data-test="add-cipher-key-akeyless-access-key-input-field"]');
|
|
this.akeylessAccessKeyError = page.locator('[data-test="add-cipher-key-akeyless-access-key-input-error"]');
|
|
|
|
// AddAkeylessType — secret type select
|
|
this.secretTypeSelect = page.locator('[data-test="add-cipher-key-secret-type-input"]');
|
|
this.secretTypeError = page.locator('[data-test="add-cipher-key-secret-type-input-error"]');
|
|
|
|
// AddAkeylessType — static secret name
|
|
this.staticSecretNameInput = page.locator('[data-test="add-cipher-key-akeyless-static-secret-name-input-field"]');
|
|
this.staticSecretNameError = page.locator('[data-test="add-cipher-key-akeyless-static-secret-name-input-error"]');
|
|
|
|
// AddEncryptionMechanism — provider type select and algorithm select (step 2)
|
|
this.encryptionProviderTypeSelect = page.locator('[data-test="add-cipher-key-auth-method-input"]');
|
|
this.encryptionProviderTypeError = page.locator('[data-test="add-cipher-key-auth-method-input-error"]');
|
|
this.encryptionProviderTypeTrigger = page.locator('[data-test="add-cipher-key-auth-method-input-trigger"]');
|
|
this.encryptionAlgorithmSelect = page.locator('[data-test="add-cipher-algorithm-input"]');
|
|
this.encryptionAlgorithmError = page.locator('[data-test="add-cipher-algorithm-input-error"]');
|
|
this.encryptionAlgorithmTrigger = page.locator('[data-test="add-cipher-algorithm-input-trigger"]');
|
|
|
|
// Toast
|
|
this.toastMessages = page.locator('[data-test="o-toast-message"]');
|
|
}
|
|
|
|
// ── Per-value option factory (runtime-dynamic value) ─────────────────────────
|
|
|
|
/**
|
|
* Returns the listbox option for a select identified by its parent data-test
|
|
* and the option's `data-test-value` (the OSelect value-specific convention).
|
|
*/
|
|
getSelectOptionByValue(parentDataTest, value) {
|
|
return this.page.locator(`[data-test="${parentDataTest}-option"][data-test-value="${value}"]`);
|
|
}
|
|
|
|
// ── Navigation ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Enterprise-availability probe. Navigates into the Cipher Keys tab and
|
|
* POSITIVELY detects whether the (enterprise-only) feature mounted, returning
|
|
* a boolean instead of throwing. Returns false on OSS where the route never
|
|
* mounts so the suite can SKIP rather than fail. Returns true on enterprise.
|
|
*/
|
|
async navigateToCipherKeysTab() {
|
|
await this.settingsMenu.waitFor({ state: 'visible' });
|
|
await this.settingsMenu.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
try {
|
|
await this.cipherKeyTab.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.cipherKeyTab.click();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async openAddCipherKeyForm() {
|
|
await this.addCipherKeyButton.waitFor({ state: 'visible' });
|
|
await this.addCipherKeyButton.click();
|
|
await this.continueButton.waitFor({ state: 'visible' });
|
|
}
|
|
|
|
// ── Name field ──────────────────────────────────────────────────────────────
|
|
|
|
async fillName(name) {
|
|
await this.nameInput.waitFor({ state: 'attached' });
|
|
await this.nameInput.fill(name, { force: true });
|
|
}
|
|
|
|
async clearName() {
|
|
await this.nameInput.waitFor({ state: 'attached' });
|
|
await this.nameInput.fill('', { force: true });
|
|
// Trigger blur so the error fires
|
|
await this.nameInput.dispatchEvent('blur');
|
|
}
|
|
|
|
async getNameError() {
|
|
return this.nameError;
|
|
}
|
|
|
|
// ── Store type ──────────────────────────────────────────────────────────────
|
|
|
|
async selectAkeylessStoreType() {
|
|
await this.storeTypeSelect.waitFor({ state: 'visible' });
|
|
await this.storeTypeSelect.click();
|
|
await this.storeTypeAkeylessOption.waitFor({ state: 'visible' });
|
|
await this.storeTypeAkeylessOption.click();
|
|
}
|
|
|
|
// ── Continue / Save ─────────────────────────────────────────────────────────
|
|
|
|
async clickContinue() {
|
|
await this.continueButton.waitFor({ state: 'visible' });
|
|
await this.continueButton.click();
|
|
}
|
|
|
|
async clickSave() {
|
|
await this.saveButton.waitFor({ state: 'visible' });
|
|
await this.saveButton.click();
|
|
}
|
|
|
|
// ── OpenObserve secret ──────────────────────────────────────────────────────
|
|
|
|
async fillSecret(secret) {
|
|
await this.secretInput.waitFor({ state: 'attached' });
|
|
await this.secretInput.fill(secret, { force: true });
|
|
}
|
|
|
|
// ── Akeyless fields ─────────────────────────────────────────────────────────
|
|
|
|
async fillAkeylessBaseUrl(url) {
|
|
await this.akeylessBaseUrlInput.waitFor({ state: 'attached' });
|
|
await this.akeylessBaseUrlInput.fill(url, { force: true });
|
|
}
|
|
|
|
async fillAkeylessAccessId(id) {
|
|
await this.akeylessAccessIdInput.waitFor({ state: 'attached' });
|
|
await this.akeylessAccessIdInput.fill(id, { force: true });
|
|
}
|
|
|
|
async fillAkeylessAccessKey(key) {
|
|
await this.akeylessAccessKeyInput.waitFor({ state: 'attached' });
|
|
await this.akeylessAccessKeyInput.fill(key, { force: true });
|
|
}
|
|
|
|
async fillStaticSecretName(name) {
|
|
await this.staticSecretNameInput.waitFor({ state: 'attached' });
|
|
await this.staticSecretNameInput.fill(name, { force: true });
|
|
}
|
|
|
|
async selectAuthMethod(value) {
|
|
await this.authMethodSelect.waitFor({ state: 'visible' });
|
|
await this.authMethodSelect.click();
|
|
const option = this.getSelectOptionByValue('add-cipher-key-auth-method-input', value);
|
|
await option.waitFor({ state: 'visible' });
|
|
await option.click();
|
|
}
|
|
|
|
async selectSecretType(value) {
|
|
await this.secretTypeSelect.waitFor({ state: 'visible' });
|
|
await this.secretTypeSelect.click();
|
|
const option = this.getSelectOptionByValue('add-cipher-key-secret-type-input', value);
|
|
await option.waitFor({ state: 'visible' });
|
|
await option.click();
|
|
}
|
|
|
|
// ── Toast assertion ─────────────────────────────────────────────────────────
|
|
|
|
async verifyToastMessage(expectedText) {
|
|
// Deterministic wait: poll the visible toast text contents until the expected
|
|
// message appears (or the assertion times out).
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const texts = await this.toastMessages.allTextContents().catch(() => []);
|
|
return texts.map((t) => (t || '').toLowerCase());
|
|
},
|
|
{ timeout: 12000, intervals: [300] },
|
|
)
|
|
.toEqual(expect.arrayContaining([expect.stringContaining(expectedText.toLowerCase())]));
|
|
}
|
|
|
|
// ── Assertion helpers ────────────────────────────────────────────────────────
|
|
|
|
async assertNameErrorVisible() {
|
|
await expect(this.nameError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertStoreTypeErrorVisible() {
|
|
await expect(this.storeTypeError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertSecretErrorVisible() {
|
|
await expect(this.secretError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertAkeylessBaseUrlErrorVisible() {
|
|
await expect(this.akeylessBaseUrlError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertAkeylessBaseUrlErrorContains(text) {
|
|
await expect(this.akeylessBaseUrlError).toContainText(text, { timeout: 5000 });
|
|
}
|
|
|
|
async assertAkeylessAccessIdErrorVisible() {
|
|
await expect(this.akeylessAccessIdError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertAkeylessAccessKeyErrorVisible() {
|
|
await expect(this.akeylessAccessKeyError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertStaticSecretNameErrorVisible() {
|
|
await expect(this.staticSecretNameError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
// ── AddEncryptionMechanism helpers ──────────────────────────────────────────
|
|
|
|
async selectEncryptionProviderType(value) {
|
|
// OSelect (reka-ui popover): clicking the root wrapper does NOT open the
|
|
// dropdown, and the trigger can open-then-close on a single click. Drive it
|
|
// through the shared helper (clicks the inner -trigger until aria-expanded).
|
|
await this.encryptionProviderTypeSelect.waitFor({ state: 'visible', timeout: 10000 });
|
|
await openOSelectDropdown(this.page, this.encryptionProviderTypeSelect);
|
|
const option = this.getSelectOptionByValue('add-cipher-key-auth-method-input', value);
|
|
await option.waitFor({ state: 'visible' });
|
|
await option.click();
|
|
}
|
|
|
|
async assertEncryptionProviderTypeErrorVisible() {
|
|
await expect(this.encryptionProviderTypeError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertEncryptionProviderTypeErrorHidden() {
|
|
await expect(this.encryptionProviderTypeError).toHaveCount(0);
|
|
}
|
|
|
|
async assertEncryptionProviderTypeSelected(label) {
|
|
await expect(this.encryptionProviderTypeTrigger).toHaveAttribute('data-test-selected-label', label, { timeout: 5000 });
|
|
}
|
|
|
|
async assertEncryptionAlgorithmErrorVisible() {
|
|
await expect(this.encryptionAlgorithmError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertEncryptionAlgorithmErrorHidden() {
|
|
await expect(this.encryptionAlgorithmError).toHaveCount(0);
|
|
}
|
|
|
|
async assertEncryptionAlgorithmSelected(label) {
|
|
await expect(this.encryptionAlgorithmTrigger).toHaveAttribute('data-test-selected-label', label, { timeout: 5000 });
|
|
}
|
|
|
|
async assertSaveButtonDisabled() {
|
|
await expect(this.saveButton).toBeDisabled({ timeout: 5000 });
|
|
}
|
|
}
|