
Cypress To Scout Migration
- 2 installs
- 21.2k repo stars
- Updated August 5, 2026
- elastic/kibana
cypress-to-scout-migration skill documents Migrate Kibana Cypress E2E tests (.
About
cypress-to-scout-migration skill documents Migrate Kibana Cypress E2E tests (.cy.ts) to Scout (Playwright). Applies to any Kibana plugin or solution. Includes triage gates (duplicate detection, layer analysis, value assessment), Cypress-to-Scout pattern mapping, data cleanup audit, and PR workflow. Use when: (1) migrating a Cypress test to S. name: cypress-to-scout-migration description: >
- Migrate Kibana Cypress E2E tests (.
- Use `test.step()` for multi-step flows to reuse browser context
- Platform-specific setup patterns for cypress-to-scout-migration.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for cypress-to-scout-migration versus alternatives.
Cypress To Scout Migration by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,788 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
cypress-to-scout-migration capabilities & compatibility
- Capabilities
- cypress to scout migration quick start · cypress to scout migration when to use guidance · cypress to scout migration integration patterns
- Works with
- elasticsearch
- Use cases
- security audit
What cypress-to-scout-migration says it does
Migrate Kibana Cypress E2E tests (.cy.ts) to Scout (Playwright). Applies to any Kibana plugin or
solution. Includes triage gates (duplicate detection, layer analysis, value assessment),
npx skills add https://github.com/elastic/kibana --skill cypress-to-scout-migrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 21.2k |
| Last updated | August 5, 2026 |
| Repository | elastic/kibana ↗ |
How do I use cypress-to-scout-migration correctly?
Migrate Kibana Cypress E2E tests (.cy.ts) to Scout (Playwright). Applies to any Kibana plugin or solution. Includes triage gates (duplicate detection, layer analysis, value assessment), Cypress-to-Sco
Who is it for?
Teams implementing cypress-to-scout-migration workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about cypress-to-scout-migration, migrate kibana cypress e2e tests (.cy.ts) to scout (playwright). applies to any kibana plu.
What you get
Working cypress-to-scout-migration setup with validated configuration and next steps.
Files
Cypress to Scout Migration
Overview
Migrate Cypress tests to Scout by first validating each test through triage gates, then rewriting using Scout patterns. Never migrate Cypress tests directly — validate first, then rewrite following Scout best practices.
Required sub-skills
- REQUIRED: scout-create-scaffold (generate Scout directory structure)
- REQUIRED: scout-ui-testing (page objects, browser auth, parallel UI)
- REQUIRED: scout-api-testing (apiClient/auth, apiServices patterns)
- REQUIRED: scout-best-practices-reviewer (review migrated tests)
Core principle
Exercise behavior in the least flaky automation layer first: UNIT > API > UI
A Cypress E2E test should only become a Scout E2E test if it genuinely tests a user workflow that cannot be verified at a lower layer.
Tools
- Scaffold a spec file:
bash scripts/scaffold_scout_spec.sh --name <name> --domain <path> --plugin-test-dir <path> [--type parallel|sequential] - Check selector validity:
bash scripts/extract_selectors.sh <cypress-test-file> --app-src <path-to-plugin-source>
All paths relative to this skill's directory.
Step 0: Load solution-specific skill [mandatory]
Before starting triage or migration, check the References section for a solution-specific extension skill that matches the Cypress test's location. If one exists, read it immediately — it overrides general conventions with solution-specific paths, packages, roles, API services, and templates. All subsequent phases must follow the solution-specific conventions when they conflict with this skill.
Phase 1: Triage (before touching any code) [medium freedom]
For each Cypress test, pass all five gates before migrating.
Gate 0: Is the feature still valid?
1. Check if the feature under test still exists in the codebase 2. Run scripts/extract_selectors.sh <test-file> to verify selectors still exist 3. Check if the feature was removed, redesigned, or moved behind a feature flag
| Finding | Action |
|---|---|
| Feature exists unchanged | Continue to Gate 1 |
| Feature redesigned | Write new Scout test from scratch (don't port) |
| Feature removed | Delete Cypress test, no migration needed |
Gate 1: Is it already covered?
Search for existing coverage in:
- Scout tests in
test/scout/ - API integration tests
- Unit tests co-located with source code
- Other Cypress tests covering same behavior
Don't rely on test names — check what the test actually asserts.
If covered at a lower layer → delete Cypress, no migration needed.
If covered in Scout → delete Cypress. Scout now runs on serverless (MKI), so Cypress tests are no longer needed for serverless (MKI) coverage.
Gate 2: Is it at the right layer?
| What the test validates | Right layer |
|---|---|
| Data transformation / API response | API test or unit test |
| Component rendering in isolation | Unit test (RTL) |
| User workflow across pages | Scout UI test |
| Permission-gated UI behavior | Scout UI test (with role-based auth) |
| Visual/cosmetic behavior | Consider deletion or visual regression tool |
If the test belongs at API/unit layer → write coverage there instead.
Gate 3: Does it add value?
Delete without migrating if the test:
- Only verifies a page loads without errors
- Only checks that a button or element exists
- Tests trivial behavior already covered by type safety
- Has been skipped for 3+ months with no progress
- Tests deprecated or soon-to-be-removed functionality
Gate 4: Flakiness risk assessment
Two checks — current status and source code risk scan.
4a: Current status
If the test is currently skipped (.skip, @skipInServerless, etc.) or chronically flaky: 1. Determine if flakiness is a test problem or an app bug 2. App bug → fix the app first, then write the Scout test 3. Cypress-specific issue (timing, selectors) → migrate with proper patterns
If the solution has a flaky-test-doctor skill, use it for deeper root cause analysis.
4b: Source code risk scan
Even if the Cypress test passes reliably today, its source code may contain patterns that would produce a flaky Scout test. Scan the Cypress file and its imported tasks/screens against the pattern catalog in references/flakiness-risk-patterns.md.
| Risk level | Action |
|---|---|
| No risky patterns | Proceed to migration |
| Medium-risk patterns | Proceed — address each pattern during rewrite (note planned remediation) |
| High/critical-risk patterns | Assess effort — may be simpler to write the test from scratch |
| App-level timing issues detected | Fix the app first, then write the Scout test |
For tests with 3+ critical/high-risk patterns, strongly consider writing the Scout test from scratch using the feature spec rather than porting the Cypress logic.
PR strategy
One Cypress spec file = one PR. Each migrated spec file must be submitted as its own pull request:
- Keeps reviews focused and manageable
- Isolates risk — a problem in one migration doesn't block others
- Makes it easy to revert a single migration if issues surface
Every PR must pass the [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner) before merging. Run the new Scout test through the flaky test suite runner to verify stability.
Phase 2: Migration
Tests that pass all triage gates proceed here. Do not port Cypress code 1:1. Rewrite using Scout patterns.
Step 1: Determine test type [high freedom]
- UI test if it verifies user flows, page rendering, or interactive behavior
- API test if the Cypress test primarily validates data via API assertions
Step 2: Set up Scout scaffold [low freedom — use script]
If the plugin doesn't have a Scout test directory yet, read the scout-create-scaffold skill.
Generate the spec file boilerplate:
bash scripts/scaffold_scout_spec.sh --name <spec_name> --domain <domain_path> \
--plugin-test-dir <plugin>/test/scout/ui --type parallelUse the solution-specific skill's page object and API service templates as starting points (if available).
Step 3: Map Cypress patterns to Scout [medium freedom]
| Cypress | Scout |
|---|---|
cy.visit() | page.gotoApp() or page object goto() |
cy.get('[data-test-subj="x"]') | page.testSubj.locator('x') |
cy.intercept() + cy.wait() | Playwright auto-waiting or expect.poll() |
cy.request() (setup/teardown) | apiServices / kbnClient in beforeAll |
cy.wait(ms) | Forbidden — use expect.poll() or locator assertions |
| Screens files (selectors) | Page object class with locators |
| Tasks files (actions) | Page object methods |
{ force: true } | Fix the underlying issue — don't port force clicks (app bugs: use dispatchEvent('click') — see best practices) |
.within() | .locator() chaining (no stale reference issues) |
beforeEach (UI setup) | apiServices in beforeAll (API-based setup) |
@ess / @serverless tags | tags.stateful.<domain>, tags.serverless.<solution>.<tier> |
ftrConfig (serverless tiers) | Scout test tags |
ftrConfig (feature flags) | Kibana Core APIs (MKI/cloud) or custom server config (stateless) |
esArchiver (system indices) | Forbidden — use kbnClient |
Step 3b: Data cleanup audit [low freedom — must be thorough]
Critical: Cypress runs each spec in a clean environment, so many Cypress tests never clean up after themselves. Scout shares the environment across specs — leftover data will break other tests. Do not trust the Cypress test's cleanup.
1. Read the Cypress test and its tasks/setup — identify every resource created:
- Saved objects (rules, cases, dashboards, saved queries)
- ES indices or documents
- Fleet agents, policies, integrations
- User preferences, UI settings, localStorage state
- API keys or credentials
2. Add explicit cleanup in the Scout test (afterAll / afterEach)
3. Add defensive cleanup in `beforeAll` — handles leftover data from a previous failed run
4. Verify cleanup works — run the test twice in a row locally. Second run fails → cleanup is incomplete.
Step 4: Write the Scout test [high freedom]
Read the scout-ui-testing or scout-api-testing skill for implementation details.
Key rules:
- Tags are required — Scout validates UI test tags at runtime
- One suite per file — single top-level
test.describe()orspaceTest.describe() - UI actions in page objects, assertions in specs
- API-based setup/teardown via
apiServices/kbnClient - Use `test.step()` for multi-step flows to reuse browser context
- Parallelize when possible — use
spaceTest+scoutSpacefor worker-isolated spaces - Test fixture for per-test isolated setup; Worker fixture for shared setup
- Page objects encapsulate selectors and actions; assertions stay in specs
- EUI wrappers — use Scout's
EuiComboBoxWrapper,EuiDataGridWrapper, etc. - All created data must be cleaned up — see Step 3b
Step 5: Review and verify [low freedom — mandatory checklist]
1. Run the scout-best-practices-reviewer skill against the new test 2. Make sure the test fails — intentionally break the feature and confirm the test catches it 3. Run locally: node scripts/scout.js run-tests --stateful --testFiles <path> 4. Update manifests: node scripts/scout.js update-test-config-manifests 5. Open a PR with only this spec file's migration (one spec per PR) 6. Run the [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner) — do not merge until stable
Phase 3: Cleanup
After the Scout test is verified:
1. Delete the Cypress test file 2. Remove orphaned code (tasks, screens, objects only used by the deleted test) 3. Check for orphaned imports (grep to verify no other usages) 4. Remove from all relevant config/index files 5. Update manifests: node scripts/scout.js update-test-config-manifests
Common mistakes
- Trusting the Cypress test's cleanup — Scout shares the env, so you must add cleanup
- Porting Cypress code line-by-line instead of rewriting with Scout patterns
- Migrating tests that belong at API/unit layer
- Forgetting triage gates and migrating flaky/invalid/duplicate tests
- Skipping the Gate 4b risk scan — Cypress patterns that seem harmless produce flaky Scout tests
- Using
page.waitForTimeout()— forbidden, same ascy.wait(ms) - Using
page.waitForLoadState('networkidle')— anti-pattern, actively removed from Scout tests; wait for specific elements instead - Using short custom timeouts on
waitFor()(e.g., 3s) — causes CI flakiness; use the default (10s) - Adding explicit waits before
clear(),fill(),click()— these auto-wait; the extra wait is redundant - Specifying
{ state: 'visible' }onwaitFor()— it's the default, omit it - Missing Scout tags (validated at runtime)
- Using
esArchiverfor system indices (usekbnClient) - Not parallelizing tests that could run in parallel
- Putting assertions in page objects (keep in specs)
- Skipping the "make sure your test fails" verification
- Batching multiple spec migrations into a single PR
- Merging without running the Flaky Test Runner
- Using
fill()on Kibana query bars —QueryStringInputsubmits React props, not DOM value; usepressSequentially()(see best practices) - Asserting
.euiTableRowcount as 0 —EuiBasicTablealways renders an empty-state row; assert on the message text instead (see best practices)
Phase 4: Skill improvement
After every migration, review what you learned and suggest updating this skill if any of these apply:
- New Kibana/EUI component interaction pattern — a component required a non-obvious Playwright approach (e.g.,
pressSequentiallyfor query bars,dispatchEventfor unstable popovers, CSS:has()for tooltip anchors) - New flakiness pattern — a Cypress pattern caused flakiness in Scout that isn't already in
references/flakiness-risk-patterns.md - New API service or page object — reusable infrastructure warrants documenting in the solution-specific skill
- New role or auth method — a convenience login method was added (e.g.,
loginAsT1Analyst) - Lint rule workaround — a Playwright lint rule required a non-obvious alternative (e.g.,
dispatchEventinstead offorce: true,toContainText([...])instead ofnth()) - Ownership or architecture insight — learned where UI code lives, which plugin owns what, or how data flows
Prompt the user: _"During this migration I learned [X]. Want me to add it to the skill so future migrations benefit?"_
References
Open only what you need:
- Key differences between Cypress and Scout (auth, CI, MKI, tags, execution model):
references/cypress-vs-scout-differences.md - Best practices for writing Scout tests (fixtures, page objects, parallelism):
references/migration-best-practices.md - Flakiness risk patterns to scan for during Gate 4b (hard-coded waits, missing cleanup, force clicks, etc.):
references/flakiness-risk-patterns.md - Complete before/after migration example with annotated decisions (Timeline creation):
references/example-migration.md
Solution-specific extensions (Step 0)
Load the matching skill when migrating tests from that solution:
- Security Solution (tests in
x-pack/solutions/security/):x-pack/solutions/security/plugins/security_solution/.agents/skills/cypress-to-scout-migration/SKILL.md
Cypress vs Scout Differences
Table of Contents
- Folder Structure
- Authentication
- CI Execution & Parallelization
- MKI Pipelines
- Local Execution
- Test Labels/Tags
- Test Patterns
- Serverless Tiers & Feature Flags
- Key Behavioral Differences
Folder Structure
| Aspect | Cypress | Scout |
|---|---|---|
| Location | Centralized test folder (test/<solution>_cypress/) | Tests closer to the plugin (<plugin>/test/scout/) |
| Organization | cypress/e2e/<domain>/ | test/scout/ui/{tests,parallel_tests}/ |
Authentication
| Aspect | Cypress | Scout |
|---|---|---|
| ESS | Basic authentication | SAML authentication |
| Serverless | SAML authentication | SAML authentication |
| Auth helpers | Custom login tasks | browserAuth.loginAsAdmin(), loginAsViewer(), loginAsPrivilegedUser(), loginWithCustomRole() |
Scout uses SAML for both ESS and Serverless, making auth consistent across environments.
CI Test Execution & Parallelization
| Aspect | Cypress | Scout |
|---|---|---|
| Environment per spec | Each spec runs in a clean environment | Spec files share the same environment |
| Data isolation | Automatic (clean environment) | Manual (different Kibana spaces via spaceTest) |
| Parallelization control | parallelism attribute in Buildkite YAML | workers attribute in Playwright config |
| Parallelization method | Spec files distributed between CI jobs | Tests run in parallel in same Kibana/ES instances, isolated by spaces |
Critical implication: In Scout, data created by one test may affect others. Use spaceTest + scoutSpace for isolation, and clean up in afterAll. Most Cypress tests have no cleanup logic because the environment is reset per spec. When migrating, you must independently audit what the test creates and add explicit cleanup.
MKI Pipelines
Scout now runs on MKI via the Appex QA Serverless Scout pipeline. Cypress tests with @serverless tags no longer need to be kept solely for MKI coverage — Scout can replace them.
Local Test Execution
| Aspect | Cypress | Scout |
|---|---|---|
| Environment setup | Cypress creates the environment automatically | You must create the environment first |
| Server start | Automatic | node scripts/scout.js start-server --arch stateful --domain classic in a separate terminal |
| Running tests | Open Cypress UI or cypress run | node scripts/scout.js run-tests --arch stateful --domain security_complete --testFiles <path> |
Test Labels/Tags
| Aspect | Cypress | Scout |
|---|---|---|
| Label type | Negative tags allowed (@skipInEss, @skipInServerless) | Positive tags only (due to Playwright's design) |
| Skip mechanism | @skipInEss, @skipInServerless, @skipInServerlessMKI | No equivalent skip tags — use positive tags to include |
| Validation | No runtime validation | Scout validates UI tags at runtime |
Test Patterns
| Cypress | Scout |
|---|---|
| Screens (selector files) | Page objects (class with locators + methods) |
| Tasks (action files) | Page object methods |
Direct cy.get() in tests | page.testSubj.locator() via page objects |
| No EUI abstraction | EUI wrappers (EuiComboBoxWrapper, EuiDataGridWrapper, etc.) |
Serverless Tiers & Feature Flags
| Aspect | Cypress | Scout |
|---|---|---|
| Serverless tier config | ftrConfig attribute in spec file | Test tags |
| Feature flags (local/CI) | ftrConfig attribute in spec file | Custom server configuration for stateless environments |
| Feature flags (MKI/cloud) | Limited — often requires @skipInServerlessMKI | Kibana Core APIs |
Key Behavioral Differences
1. Spec isolation: Cypress creates a clean environment per spec file. Scout shares the environment — you must manage state explicitly.
2. Auto-waiting: Cypress retries commands with built-in timeout. Playwright/Scout also auto-waits on locator actions but uses different mechanisms (expect.poll(), locator assertions).
3. Async model: Cypress chains commands in a queue. Scout/Playwright uses async/await — more predictable control flow.
4. Browser context: In Scout, each test() block spins up a new browser context. Use test.step() for multi-step flows to reuse context and improve execution time.
5. Setup/teardown: Cypress often uses UI for setup. Scout strongly prefers API-based setup via apiServices/kbnClient.
Example Migration: Timeline Creation
Real migration of investigations/timelines/creation.cy.ts to Scout. Demonstrates triage decisions, pattern mapping, page object design, API service design, and cleanup strategy.
Triage summary
| Gate | Result |
|---|---|
| Gate 0: Feature valid? | Yes — timeline creation UI unchanged |
| Gate 1: Already covered? | No Scout or API test covers these flows |
| Gate 2: Right layer? | UI — tests user workflows (create, save, RBAC, state lifecycle) |
| Gate 3: Adds value? | Yes — validates save states, RBAC, template creation |
| Gate 4: Flakiness risk? | Medium — { force: true } on collapsed actions button (app bug), LOADING_INDICATOR waits |
Before: Cypress (creation.cy.ts, abbreviated)
import { ROWS } from '../../../screens/timelines';
import { deleteTimelines, createTimelineTemplate } from '../../../tasks/api_calls/timelines';
import { login } from '../../../tasks/login';
import { addNameToTimelineAndSave, executeTimelineKQL, closeTimeline } from '../../../tasks/timeline';
describe('Timelines', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
deleteTimelines(); // API cleanup — no afterEach
});
it('should show the different timeline states', () => {
login();
visitWithTimeRange(TIMELINES_URL);
openTimelineUsingToggle();
cy.get(TIMELINE_STATUS).invoke('text').should('match', /^Unsaved/);
addNameToTimelineAndSave('Test');
cy.get(TIMELINE_STATUS).should('not.exist');
cy.get(LOADING_INDICATOR).should('be.visible'); // Sync on background save
cy.get(LOADING_INDICATOR).should('not.exist');
executeTimelineKQL('agent.name : *');
cy.get(TIMELINE_STATUS).invoke('text').should('match', /^Unsaved changes/);
});
it('should save timelines as new', () => {
login();
visitWithTimeRange(TIMELINES_URL);
cy.get(ROWS).should('have.length', '0'); // EuiBasicTable quirk — see below
openTimelineUsingToggle();
addNameToTimelineAndSave('First');
cy.get(LOADING_INDICATOR).should('be.visible');
cy.get(LOADING_INDICATOR).should('not.exist');
addNameToTimelineAndSaveAsNew('Second');
closeTimeline();
cy.get(ROWS).should('have.length', '2');
cy.get(ROWS).first().invoke('text').should('match', /Second/); // .first()/.last() — see below
cy.get(ROWS).last().invoke('text').should('match', /First/);
});
});Key problems in the Cypress source
| Pattern | Risk | Scout approach |
|---|---|---|
LOADING_INDICATOR wait | Critical — hard-coded UI sync point | Replaced with waitForSaveComplete() using expect.poll() or locator assertion |
No afterEach cleanup | Critical — Scout shares environment | Added beforeEach + afterAll cleanup via apiServices.timeline.deleteAll() |
.first() / .last() on rows | High — forbidden by playwright/no-nth-methods | Replaced with toContainText(['Second', 'First']) (ordered array) |
cy.get(ROWS).should('have.length', '0') | Medium — EuiBasicTable always renders an empty-state row | Assert on empty-state message text instead |
Selectors in separate screens/ files | Structural | Moved to page object readonly properties |
Actions in separate tasks/ files | Structural | Moved to page object methods |
After: Scout (split into two files — one role per file)
The migration splits tests by role: CRUD tests in timeline_creation.spec.ts (platform engineer) and read-only tests in timeline_read_only.spec.ts (T1 analyst). Each file is self-contained with its own setup/teardown and login in beforeEach.
timeline_creation.spec.ts (CRUD role)
import { spaceTest, tags } from '@kbn/scout-security';
import { expect } from '@kbn/scout-security/ui';
spaceTest.describe(
'Timeline creation',
{ tag: [...tags.stateful.classic, ...tags.serverless.security.complete] },
() => {
spaceTest.beforeEach(async ({ browserAuth, apiServices, pageObjects }) => {
await apiServices.timeline.deleteAll();
await browserAuth.loginAsPlatformEngineer();
await pageObjects.timelinePage.navigateToTimelines();
});
spaceTest.afterAll(async ({ apiServices }) => {
await apiServices.timeline.deleteAll();
});
spaceTest('should show the different timeline states', async ({ pageObjects }) => {
const { timelinePage } = pageObjects;
await timelinePage.open();
await spaceTest.step('Verify unsaved state', async () => {
await expect(timelinePage.saveStatus).toHaveText(/^Unsaved/);
});
await spaceTest.step('Save and verify saved state', async () => {
await timelinePage.saveWithName('Test');
await expect(timelinePage.saveStatus).toBeHidden();
});
await spaceTest.step('Modify query and verify unsaved changes', async () => {
await timelinePage.executeKQL('agent.name : *');
await expect(timelinePage.saveStatus).toHaveText(/^Unsaved changes/);
});
});
spaceTest('should save timelines as new', async ({ pageObjects }) => {
const { timelinePage } = pageObjects;
await spaceTest.step('Verify empty state', async () => {
await expect(timelinePage.timelinesTable).toContainText(
'0 timelines match the search criteria'
);
});
await spaceTest.step('Create, save, and save as new', async () => {
await timelinePage.open();
await timelinePage.saveWithName('First');
await expect(timelinePage.saveStatus).toBeHidden();
await timelinePage.saveAsNew('Second');
});
await spaceTest.step('Verify both timelines in list', async () => {
await timelinePage.close();
await expect(timelinePage.timelineRows).toHaveCount(2);
await expect(timelinePage.timelineRows).toContainText(['Second', 'First']);
});
});
}
);timeline_read_only.spec.ts (read-only role)
import { spaceTest, tags } from '@kbn/scout-security';
import { expect } from '@kbn/scout-security/ui';
spaceTest.describe(
'Timeline read-only',
{ tag: [...tags.stateful.classic, ...tags.serverless.security.complete] },
() => {
spaceTest.beforeEach(async ({ browserAuth, apiServices, pageObjects }) => {
await apiServices.timeline.deleteAll();
await browserAuth.loginAsT1Analyst();
await pageObjects.timelinePage.navigateToTimelines();
});
spaceTest.afterAll(async ({ apiServices }) => {
await apiServices.timeline.deleteAll();
});
spaceTest(
'should not be able to create/update timeline with only read privileges',
async ({ pageObjects }) => {
const { timelinePage } = pageObjects;
await timelinePage.open();
await timelinePage.createNew();
await expect(timelinePage.panel).toBeVisible();
await expect(timelinePage.saveButton).toBeDisabled();
await spaceTest.step('Hover save button and verify read-only tooltip', async () => {
await timelinePage.hoverSaveButton();
await expect(timelinePage.saveTooltip).toContainText(
'you do not have the required permissions to save timelines'
);
});
}
);
}
);Key decisions annotated
| Decision | Why |
|---|---|
spaceTest (not test) | Enables parallel execution — each worker gets its own Kibana space |
| One role per file | Simulates a realistic user flow — each file = one role, one full-flow |
Login + navigation in beforeEach | Shared setup across all tests in the file — avoids duplication |
browserAuth.loginAsPlatformEngineer() | Least-privileged role for CRUD. Not loginAsAdmin() (masks permission bugs) |
browserAuth.loginAsT1Analyst() | Read-only RBAC test — verifies save button is disabled |
spaceTest.step() for multi-step flows | Reuses browser context within a single test (each spaceTest() creates a new context) |
beforeEach + afterAll cleanup | beforeEach handles prior failed runs; afterAll cleans up after the suite |
apiServices.timeline.deleteAll() | API-based cleanup — not UI-based (faster, more reliable) |
toContainText(['Second', 'First']) | Ordered array assertion — replaces .first() / .last() (forbidden by playwright/no-nth-methods) |
Page object: TimelinePage (abbreviated)
export class TimelinePage {
// All locators as readonly constructor properties — centralized, auditable
readonly panel: Locator;
readonly saveStatus: Locator;
readonly saveButton: Locator;
readonly kqlTextarea: Locator;
readonly saveButtonTooltipAnchor: Locator;
constructor(private readonly page: ScoutPage) {
this.panel = this.page.testSubj.locator('timeline-modal-header-panel');
// saveStatus scoped to panel — avoids strict mode violation (appears in header AND bottom bar)
this.saveStatus = this.panel.locator('[data-test-subj="timeline-save-status"]');
this.kqlTextarea = this.page.testSubj
.locator('timeline-search-or-filter-search-container')
.locator('textarea');
// CSS :has() for parent selection — EUI wraps disabled buttons in a tooltip anchor <span>
this.saveButtonTooltipAnchor = this.page.locator(
'span:has([data-test-subj="timeline-modal-save-timeline"])'
);
}
async executeKQL(query: string) {
await this.kqlTextarea.click();
await this.kqlTextarea.clear();
// pressSequentially — QueryStringInput submits React props on Enter, not DOM value.
// fill() sets DOM value synchronously but React props update asynchronously.
await this.kqlTextarea.pressSequentially(query);
await this.kqlTextarea.press('Enter');
}
async hoverSaveButton() {
// EUI wraps disabled buttons in a tooltip anchor that intercepts pointer events.
await this.saveButtonTooltipAnchor.hover();
}
}Patterns demonstrated
| Pattern | Where | Why |
|---|---|---|
| Scoped locators | saveStatus scoped to panel | Avoids strict mode violation when same data-test-subj appears in multiple DOM locations |
CSS :has() for parent selection | saveButtonTooltipAnchor | EUI wraps disabled buttons — hover the wrapper, not the button. Avoids XPath. |
pressSequentially for query bars | executeKQL() | QueryStringInput React prop sync race — fill() + Enter submits stale value |
| Private helpers | openSaveModalAndSetTitle(), confirmSaveModal() | Shared logic between saveWithName(), saveAsNew(), addNameAndDescription() |
API service: TimelineApiService (abbreviated)
export const getTimelineApiService = ({
kbnClient, log, scoutSpace,
}: {
kbnClient: KbnClient;
log: ScoutLogger;
scoutSpace?: ScoutParallelWorkerFixtures['scoutSpace']; // Space-aware for parallel tests
}): TimelineApiService => {
const basePath = scoutSpace?.id ? `/s/${scoutSpace.id}` : '';
return {
createTimeline: async (input = {}) => { /* POST to /api/timeline */ },
createTimelineTemplate: async (input = {}) => { /* POST with timelineType: 'template' */ },
deleteAll: async () => {
// Must fetch and delete both 'default' and 'template' types separately
const [defaultIds, templateIds] = await Promise.all([
fetchAllSavedObjectIds('default'),
fetchAllSavedObjectIds('template'),
]);
// ...
},
};
};Key design decisions
| Decision | Why |
|---|---|
Space-aware basePath | Supports spaceTest parallel execution — requests go to the worker's isolated space |
deleteAll() fetches both types | Timelines and templates use the same API but different timeline_type — must delete both |
measurePerformanceAsync wrapper | Built-in Scout performance instrumentation |
| Default values with spread override | { ...DEFAULT_TIMELINE, ...input } — callers only specify what they need |
Flakiness Risk Patterns for Migration
Table of Contents
- Critical — Will cause flakiness in Scout
- High — Likely to cause issues
- Medium — Address during rewrite
- How to use this during triage
Scan Cypress source code for these patterns before migration. Each indicates a risk area that needs specific handling in the Scout rewrite. Check the test file and its imported tasks/screens/objects.
Critical — Will cause flakiness in Scout
Hard-coded waits
- Look for:
cy.wait(number)(e.g.,cy.wait(500),cy.wait(2000)) - Why: The underlying timing issue the wait masks must be addressed.
page.waitForTimeout()is forbidden in Scout. - Scout approach: Playwright auto-waiting,
expect.poll(), or locator assertions with built-in retry.
Missing cleanup / shared state
- Look for: Tests with no
afterEach/aftercleanup;esArchiverLoadwithout unload; API resources created but never deleted; global mutable state. - Why: Cypress runs each spec in a clean browser. Scout shares the environment across specs in a worker — leftover data causes cascading failures.
- Scout approach: Explicit cleanup in
afterAll/afterEach, defensive cleanup inbeforeAll, unique identifiers per worker (scoutSpace.id).
Force interactions
- Look for:
{ force: true }on.click(),.type(),.check(),.select() - Why: Playwright strict mode rejects interactions with hidden/disabled elements. Force-clicks mask real UI issues (element behind overlay, not yet visible, disabled).
- Scout approach: Wait for the element to be actionable. If behind an overlay, close the overlay first.
- Exception: If the Cypress
{ force: true }exists because an app bug causes continuous DOM re-rendering (e.g., auseEffectloop triggering table re-fetches), usedispatchEvent('click')to bypass actionability checks without triggering theplaywright/no-force-optionlint rule. Document the app bug location and consider filing a fix. Seemigration-best-practices.md→ "dispatchEvent for app-level DOM instability".
esArchiver for system indices
- Look for:
cy.task('esArchiverLoad', ...)targeting system index names (.kibana,.alerts,.fleet, etc.) - Why: Forbidden in Scout. System indices are managed by Kibana and must be created via APIs.
- Scout approach: Use
kbnClientorapiServicesto create saved objects and configuration.
High — Likely to cause issues
cy.intercept() + cy.wait('@alias') as sync points
- Look for:
cy.intercept('GET|POST', '/api/...').as('alias')followed bycy.wait('@alias') - Why: Playwright doesn't have Cypress-style request interception for synchronization. Direct ports using
page.waitForResponse()are fragile. - Scout approach: Wait for UI state instead:
expect(locator).toBeVisible(),expect.poll(), or data-loading indicators.
recurse() or retry loops
- Look for:
recurse(),cy.waitUntil(), manual retry loops,.should()chains wrapping re-tried actions - Why: Indicates unstable UI or race condition in the app. The instability carries over to Scout.
- Scout approach: Use
expect.poll()orexpect(locator).toPass(). Fix the underlying instability if it's an app bug.
Index-based selectors
- Look for:
.eq(0),.first(),.last(),:nth-child()withoutdata-test-subj - Why: Fragile when tests run in parallel with dynamic data. Element order may differ between runs.
- Scout approach: Use
data-test-subjattributes. If elements are dynamic, filter by text content or unique attributes.
beforeEach with UI navigation for setup
- Look for:
beforeEach(() => { cy.visit(URL); ... })with repeated page navigation or UI-driven data creation - Why: Slow and flaky in Scout. Each navigation costs time and introduces timing risks.
- Scout approach: API-based setup in
beforeAll(worker fixture). Navigate once, usetest.step()for multi-step flows.
.within() on re-rendering containers
- Look for:
.within(() => { ... })targeting tables, lists, or containers with loading states - Why:
.within()captures a DOM snapshot that goes stale on re-render. Playwright locator chaining avoids this, but the re-rendering itself signals timing-sensitive UI. - Scout approach: Use Playwright locator chaining:
page.testSubj.locator('container').locator('child'). Locators auto-retry.
Medium — Address during rewrite
cy.request() in test body
- Look for:
cy.request({ method: 'POST', url: '/api/...', ... })insideit()blocks (not justbefore/beforeEach) - Why: In-test API calls should be in
apiServices. Mixing UI and API in the test body makes tests harder to maintain and debug. - Scout approach: Extract to
apiServicesorkbnClientcalls in setup/teardown.
localStorage / sessionStorage manipulation
- Look for:
cy.window().then(win => win.localStorage...), storage key references - Why: Scout's
spaceTestuses isolated Kibana spaces. Storage keys may differ. Async persistence can race with page reloads. - Scout approach: Use API-based state management. If storage is needed, verify persistence with polling.
cy.task() for server-side operations
- Look for:
cy.task('esArchiverLoad'),cy.task('createSignalsIndex'), custom task plugins - Why: No equivalent in Scout. Tasks are server-side Node.js functions.
- Scout approach: Replace with
kbnClient.request(),esClient, orapiServices.
Conditional test logic based on environment
- Look for:
@skipInServerless,@skipInEss,if (isServerless),Cypress.env('IS_SERVERLESS') - Why: Scout handles environment targeting with tags, not runtime conditionals. Conditional logic suggests the test may not be portable as-is.
- Scout approach: Use Scout tags for environment filtering. Split into separate tests if behavior diverges.
Deeply nested .should() chains
- Look for:
.should('be.visible').and('contain', 'text').and('have.attr', 'href', '/path') - Why: Multiple assertions on a single element work differently in Playwright. Each assertion needs its own
expect(). - Scout approach: Separate
expect()calls:await expect(locator).toBeVisible(),await expect(locator).toContainText('text').
cy.clock() / cy.tick() for time manipulation
- Look for:
cy.clock(),cy.tick(), fake timers - Why: Playwright has its own clock API (
page.clock) with different semantics. Direct port is error-prone. - Scout approach: Use
page.clock.install()/page.clock.fastForward(), or redesign to avoid fake timers.
How to use this during triage
1. Read the Cypress test file and all imported helpers (tasks, screens, objects) 2. Check each pattern against the source code 3. Record which patterns are present and their risk level 4. For critical/high patterns: note specific lines and planned Scout remediation 5. For tests with 3+ critical/high patterns: consider writing from scratch rather than porting 6. Include the risk scan findings in the triage summary before proceeding to migration
Migration Best Practices
Table of Contents
- Testing Layer Priority
- Waits and Assertions
- Page Objects
- API-Based Setup/Teardown
- test.step() for Execution Time
- Parallelization
- Fixtures
- Package Organization
- EUI Wrappers
Testing Layer Priority
| Layer | Use For | Flake Risk |
|---|---|---|
| Unit (RTL, Jest) | Component rendering, hooks, utilities | Lowest |
| API (Scout API, integration) | Data validation, API contracts, RBAC | Low |
| UI (Scout UI) | User workflows, page interactions, E2E flows | Higher |
Only use Scout UI tests for behavior that genuinely requires a browser.
Waits and Assertions
// Forbidden
await page.waitForTimeout(2000);
await page.waitForLoadState('networkidle'); // Anti-pattern — actively removed from Scout tests
// Locator assertions auto-retry
await expect(page.testSubj.locator('myElement')).toBeVisible();
// Poll for async conditions
await expect.poll(async () => {
return await page.testSubj.locator('alertRow').count();
}).toBeGreaterThan(0);waitFor() defaults
{ state: 'visible' }is the default — omit it:await element.waitFor()notawait element.waitFor({ state: 'visible' })- Don't use short custom timeouts (e.g., 3s) — they cause CI flakiness. Use the default (10s) unless there is a strong, documented reason.
Built-in auto-waiting
Many Playwright actions auto-wait before executing. Do not add explicit waits before these:
click(),fill(),clear(),press(),type(),check(),selectOption()waitFor()is only needed when you want to assert readiness without performing an action
Page Objects
Extract all locators as `readonly` properties initialized in the constructor — never create locators inline in methods. This keeps selectors centralized and makes them easy to audit or update.
class DashboardPage {
readonly riskScoreTable: Locator;
readonly enableEntityStoreButton: Locator;
constructor(private readonly page: ScoutPage) {
this.riskScoreTable = this.page.testSubj.locator('entity-analytics-risk-score');
this.enableEntityStoreButton = this.page.testSubj.locator('enable-entity-store-btn');
}
async goto() {
await this.page.gotoApp('securitySolution:entity_analytics');
}
async enableEntityStore() {
await this.enableEntityStoreButton.click();
}
}Split large pages into smaller page objects or component objects. Assertions stay in specs, not page objects.
If a needed data-test-subj doesn't exist, add it to the source component.
Locator preferences
In order of preference:
1. `page.testSubj.locator('...')` — data-test-subj attributes, most stable 2. `getByRole('row')`, `getByRole('button', { name: '...' })` — ARIA roles, semantic and resilient to class changes 3. CSS `:has()` for parent selection — page.locator('span:has([data-test-subj="..."])') over locator('xpath=..') 4. Scoped locators — parent.locator('[data-test-subj="child"]') to avoid strict mode violations
Avoid:
- EUI CSS class selectors (
.euiTableRow,.euiToolTipAnchor) — these are internal and can change between EUI versions - XPath (
xpath=..) — less readable, prefer CSS:has()for parent selection - Unscoped locators when the same
data-test-subjappears in multiple DOM locations
API-Based Setup/Teardown
spaceTest.beforeAll(async ({ apiServices }) => {
await apiServices.ruleService.createRule(ruleConfig);
});
spaceTest.afterAll(async ({ apiServices, scoutSpace }) => {
await apiServices.ruleService.deleteAllRules();
await scoutSpace.savedObjects.cleanStandardList();
});Do not use esArchiver to manipulate system indices — use kbnClient.
test.step() for Execution Time
Each test() block creates a new browser context. Use test.step() for multi-step flows to reuse context:
spaceTest('full workflow', async ({ pageObjects }) => {
await spaceTest.step('create entity', async () => {
await pageObjects.entityStore.createEntity(entityConfig);
});
await spaceTest.step('verify entity appears', async () => {
await expect(pageObjects.dashboard.entityRow(entityConfig.name)).toBeVisible();
});
});Parallelization
Parallel test runs are encouraged but have trade-offs:
- Test suites must be Space-isolated (
spaceTest+scoutSpace) - Kibana archive ingestion must be done within the test suite file, not in the global setup hook
- Kibana / ES may be slower because multiple workers ingest and interact with the UI concurrently
Use spaceTest + scoutSpace — each worker gets its own Kibana space.
- Pre-ingest shared ES data in
parallel_tests/global.setup.tsviaglobalSetupHook() - Clean up space-scoped mutations in
afterAll - Place parallel specs in
test/scout*/ui/parallel_tests/ - Place sequential specs in
test/scout*/ui/tests/
File size and role separation
- Keep spec files focused and small: aim for 4–5 short test scenarios or 2–3 long scenarios per file. This is critical for parallel execution, where the test runner balances work at the spec-file level — oversized specs create bottlenecks.
- Keep one role per file to simulate a realistic user flow. If tests use different auth roles (e.g., CRUD vs read-only), split them into separate spec files with the appropriate login in each file's
beforeEach.
Fixtures
Test fixture — each test gets a fresh, isolated instance:
export const test = baseTest.extend<MyFixtures>({
myFixture: async ({}, use) => {
const resource = await createResource();
await use(resource);
await cleanupResource(resource);
},
});Worker fixture — shared across tests within the same worker:
export const test = baseTest.extend<{}, MyWorkerFixtures>({
sharedService: [async ({}, use) => {
const service = await initService();
await use(service);
}, { scope: 'worker' }],
});Package Organization
| Package | Use For |
|---|---|
@kbn/scout | Code usable across all solutions |
@kbn/scout-security | Security-specific code |
Put shared code in @kbn/scout, security-specific code in @kbn/scout-security.
EUI Wrappers
Scout provides wrappers for stable EUI interactions — import from @kbn/scout: EuiComboBoxWrapper, EuiDataGridWrapper, EuiSelectableWrapper, EuiCheckBoxWrapper, EuiFieldTextWrapper, EuiCodeBlockWrapper, EuiSuperSelectWrapper, EuiToastWrapper
Kibana Component Interaction Patterns
Patterns learned from real migrations. These apply across all Kibana plugins, not just Security.
Kibana query bar (QueryStringInput)
The unified SearchBar's QueryStringInput submits this.props.query (the React prop) on Enter — not the DOM textarea value. Playwright's fill() sets the DOM value synchronously, but React's props update asynchronously. If press('Enter') fires before props sync, the component submits the stale (old) query and the change never takes effect.
// Broken — fill() races with React prop sync
await textarea.fill('host.name: *');
await textarea.press('Enter'); // submits stale props.query (empty string)
// Working — pressSequentially types character-by-character, giving React time
await textarea.click();
await textarea.clear();
await textarea.pressSequentially('host.name: *');
await textarea.press('Enter');This applies to any QueryStringInput in Kibana (Timeline, Discover, rule builders, etc.).
EuiBasicTable empty-state row
EuiBasicTable always renders a <tr class="euiTableRow"> for its "no items found" message. You cannot assert .euiTableRow count as 0 — the empty-state row is always present.
// Broken — always finds at least 1 row (the empty-state row)
await expect(table.locator('.euiTableRow')).toHaveCount(0);
// Working — assert the empty-state message text
await expect(table).toContainText('0 timelines match the search criteria');When the table has actual data rows, the empty-state row is not rendered, so row counts > 0 work normally.
EUI disabled button tooltip
EUI wraps disabled buttons in a tooltip anchor <span> that intercepts pointer events. To trigger the tooltip on hover, target the wrapper element, not the button.
// Broken — hover never reaches the disabled button
await saveButton.hover();
// Working — hover the tooltip anchor wrapper using CSS :has()
const tooltipAnchor = page.locator('span:has([data-test-subj="save-button"])');
await tooltipAnchor.hover();
await expect(tooltip).toBeVisible();Scoping locators to avoid strict mode violations
Some elements (e.g., save-status badges, action buttons) appear in multiple DOM locations. Scope locators to the relevant container to avoid Playwright strict mode violations.
// Risky — may match elements in the bottom bar AND the header panel
readonly saveStatus = this.page.testSubj.locator('timeline-save-status');
// Safe — scoped to the header panel
readonly saveStatus = this.panel.locator('[data-test-subj="timeline-save-status"]');dispatchEvent for app-level DOM instability
When an application bug causes continuous DOM re-rendering (e.g., a useEffect loop triggering table refetch()), elements inside affected containers get detached before Playwright's actionability checks complete. Use dispatchEvent('click') instead of force: true — it bypasses actionability checks without triggering the playwright/no-force-option lint rule.
// EUI's collapsed actions popover re-renders continuously due to
// app bug in StatefulOpenTimeline: useEffect on noteIds triggers refetch().
// See: open_timeline/index.tsx lines ~406-419
await this.createFromTemplateButton.dispatchEvent('click');Always document the app bug and the affected source location. This is distinct from porting Cypress { force: true } blindly.
Avoid .first(), .last(), .nth() — use specific locators
The playwright/no-nth-methods lint rule forbids positional methods. Alternatives:
// Forbidden — positional indexing
await actionsButton.first().click();
await rows.nth(0).toContainText('Second');
// For waitFor() — remove .first(), waitFor doesn't enforce strict mode
await actionsButton.waitFor({ state: 'visible' });
// For click() — ensure the locator matches a single element
// (e.g., scope to a specific table tab where only one row exists)
await actionsButton.click();
// For ordered assertions — toContainText accepts an array
await expect(rows).toContainText(['Second', 'First']);
// For filtering — use filter() instead of nth()
await rows.filter({ hasText: 'Security Timeline' }).click();toContainText with an array checks that each element in the locator list contains the corresponding text in order — same ordering guarantee as nth() without positional indexing.
#!/usr/bin/env bash
# Extracts data-test-subj selectors from a Cypress test and its imported screens files,
# then checks which selectors still exist in application source code.
#
# Usage:
# bash .agents/skills/cypress-to-scout-migration/scripts/extract_selectors.sh \
# <cypress-test-file> --app-src <path-to-plugin-source>
#
# Output: list of selectors with existence status (FOUND / MISSING)
set -euo pipefail
APP_SRC=""
TEST_FILE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--app-src)
APP_SRC="$2"
shift 2
;;
*)
TEST_FILE="$1"
shift
;;
esac
done
if [[ -z "$TEST_FILE" ]]; then
echo "Usage: $0 <cypress-test-file> --app-src <path-to-plugin-source>"
echo ""
echo " --app-src Path to the plugin source directory to search for selectors"
exit 1
fi
if [[ -z "$APP_SRC" ]]; then
echo "Error: --app-src is required (e.g., --app-src x-pack/solutions/security/plugins/security_solution)"
exit 1
fi
if [[ ! -f "$TEST_FILE" ]]; then
echo "Error: File not found: $TEST_FILE"
exit 1
fi
TEST_DIR=$(dirname "$TEST_FILE")
# Walk up to find the cypress root (directory containing 'screens' folder)
CYPRESS_ROOT="$TEST_DIR"
while [[ ! -d "${CYPRESS_ROOT}/screens" && "$CYPRESS_ROOT" != "/" ]]; do
CYPRESS_ROOT=$(dirname "$CYPRESS_ROOT")
done
if [[ ! -d "${CYPRESS_ROOT}/screens" ]]; then
echo "Warning: Could not find cypress/screens/ directory"
CYPRESS_ROOT="$TEST_DIR"
fi
# Collect imported files (screens and tasks)
collect_imported_files() {
local source_file="$1"
local pattern="$2"
local source_dir
source_dir=$(dirname "$source_file")
grep "from '" "$source_file" 2>/dev/null | grep "$pattern" | while read -r line; do
import_path=$(echo "$line" | sed -n "s/.*from ['\"]\\([^'\"]*\\)['\"].*/\\1/p")
if [[ -n "$import_path" ]]; then
resolved="${source_dir}/${import_path}.ts"
if [[ -f "$resolved" ]]; then
echo "$resolved"
fi
fi
done
}
SCREEN_FILES=()
TASK_FILES=()
# Collect screens imported by the test
while IFS= read -r f; do
[[ -n "$f" ]] && SCREEN_FILES+=("$f")
done < <(collect_imported_files "$TEST_FILE" "screens")
# Collect tasks imported by the test
while IFS= read -r f; do
[[ -n "$f" ]] && TASK_FILES+=("$f")
done < <(collect_imported_files "$TEST_FILE" "tasks")
# Collect screens imported by tasks
for task_file in "${TASK_FILES[@]}"; do
while IFS= read -r f; do
[[ -n "$f" ]] && SCREEN_FILES+=("$f")
done < <(collect_imported_files "$task_file" "screens")
done
# Deduplicate
SCREEN_FILES=($(printf '%s\n' "${SCREEN_FILES[@]}" | sort -u))
ALL_FILES=("$TEST_FILE" "${SCREEN_FILES[@]}" "${TASK_FILES[@]}")
# Extract data-test-subj values from all files
SELECTORS=""
for f in "${ALL_FILES[@]}"; do
if [[ -f "$f" ]]; then
# data-test-subj="value"
vals=$(grep -oE 'data-test-subj="[^"]+"' "$f" 2>/dev/null | sed 's/data-test-subj="//;s/"//' || true)
SELECTORS="${SELECTORS}${vals:+$'\n'}${vals}"
# getDataTestSubjectSelector('value')
vals=$(grep -oE "getDataTestSubjectSelector\(['\"][^'\"]+['\"]\)" "$f" 2>/dev/null | sed "s/getDataTestSubjectSelector(['\"]//;s/['\"])//" || true)
SELECTORS="${SELECTORS}${vals:+$'\n'}${vals}"
# getDataTestSubjectSelectorStartWith('value')
vals=$(grep -oE "getDataTestSubjectSelectorStartWith\(['\"][^'\"]+['\"]\)" "$f" 2>/dev/null | sed "s/getDataTestSubjectSelectorStartWith(['\"]//;s/['\"])//" || true)
SELECTORS="${SELECTORS}${vals:+$'\n'}${vals}"
fi
done
ALL_SELECTORS=$(echo "$SELECTORS" | sort -u | grep -v '^$' || true)
if [[ -z "$ALL_SELECTORS" ]]; then
echo "No data-test-subj selectors found in test or its imports."
exit 0
fi
echo "=== Selector Analysis ==="
echo "Test file: $TEST_FILE"
echo "Screen files: ${SCREEN_FILES[*]:-none}"
echo "Task files: ${TASK_FILES[*]:-none}"
echo ""
echo "--- Selectors ---"
FOUND=0
MISSING=0
while IFS= read -r selector; do
[[ -z "$selector" ]] && continue
# Skip dynamic selectors (contain ${...})
if echo "$selector" | grep -q '\$'; then
echo " DYNAMIC $selector"
continue
fi
# Search in app source (not test files)
if grep -rq "${selector}" "$APP_SRC" --include='*.ts' --include='*.tsx' --exclude-dir='test' --exclude-dir='__tests__' --exclude='*.test.*' --exclude='*.spec.*' 2>/dev/null; then
echo " FOUND $selector"
FOUND=$((FOUND + 1))
else
echo " MISSING $selector"
MISSING=$((MISSING + 1))
fi
done <<< "$ALL_SELECTORS"
TOTAL=$(echo "$ALL_SELECTORS" | wc -l | tr -d ' ')
echo ""
echo "Summary: ${FOUND} found, ${MISSING} missing, ${TOTAL} total"
if [[ $MISSING -gt 0 ]]; then
echo ""
echo "WARNING: Missing selectors may indicate removed features or renamed test-subj attributes."
echo "Verify each MISSING selector before migration."
fi
#!/usr/bin/env bash
# Generates a Scout spec file with proper boilerplate for any Kibana plugin.
#
# Usage:
# bash .agents/skills/cypress-to-scout-migration/scripts/scaffold_scout_spec.sh \
# --name timeline_creation \
# --domain investigations/timelines \
# --plugin-test-dir x-pack/solutions/security/plugins/security_solution/test/scout/ui \
# --type parallel
#
# Options:
# --name Spec name in snake_case (required)
# --domain Subdirectory path under parallel_tests/ or tests/ (required)
# --plugin-test-dir Path to the plugin's test/scout/ui directory (required)
# --type "parallel" (default) or "sequential"
# --scout-package Scout package to import from (default: @kbn/scout)
set -euo pipefail
SPEC_NAME=""
DOMAIN=""
TEST_TYPE="parallel"
PLUGIN_TEST_DIR=""
SCOUT_PACKAGE="@kbn/scout-security"
while [[ $# -gt 0 ]]; do
case "$1" in
--name) SPEC_NAME="$2"; shift 2 ;;
--domain) DOMAIN="$2"; shift 2 ;;
--plugin-test-dir) PLUGIN_TEST_DIR="$2"; shift 2 ;;
--type) TEST_TYPE="$2"; shift 2 ;;
--scout-package) SCOUT_PACKAGE="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [[ -z "$SPEC_NAME" || -z "$DOMAIN" || -z "$PLUGIN_TEST_DIR" ]]; then
echo "Error: --name, --domain, and --plugin-test-dir are required"
echo "Usage: $0 --name <spec_name> --domain <domain_path> --plugin-test-dir <path> [--type parallel|sequential] [--scout-package <pkg>]"
exit 1
fi
if [[ "$TEST_TYPE" == "parallel" ]]; then
TARGET_DIR="${PLUGIN_TEST_DIR}/parallel_tests/${DOMAIN}"
TEST_FN="spaceTest"
else
TARGET_DIR="${PLUGIN_TEST_DIR}/tests/${DOMAIN}"
TEST_FN="test"
fi
SPEC_FILE="${TARGET_DIR}/${SPEC_NAME}.spec.ts"
if [[ -f "$SPEC_FILE" ]]; then
echo "Error: ${SPEC_FILE} already exists"
exit 1
fi
mkdir -p "$TARGET_DIR"
cat > "$SPEC_FILE" << TEMPLATE
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
TEMPLATE
if [[ "$TEST_TYPE" == "parallel" ]]; then
cat >> "$SPEC_FILE" << PARALLEL
import { spaceTest, tags } from '${SCOUT_PACKAGE}';
import { expect } from '${SCOUT_PACKAGE}/ui';
spaceTest.describe(
'TODO: describe suite',
{ tag: [/* TODO: add tags e.g. ...tags.stateful.classic */] },
() => {
spaceTest.beforeEach(async ({ browserAuth, apiServices, scoutSpace }) => {
// TODO: API-based setup
await browserAuth.loginAsAdmin();
});
spaceTest.afterEach(async ({ apiServices }) => {
// TODO: clean up ALL created data
});
spaceTest('TODO: test name', async ({ pageObjects, page }) => {
await spaceTest.step('TODO: first step', async () => {
// TODO: implement
});
});
}
);
PARALLEL
else
cat >> "$SPEC_FILE" << SEQUENTIAL
import { test, tags } from '${SCOUT_PACKAGE}';
import { expect } from '${SCOUT_PACKAGE}/ui';
test.describe(
'TODO: describe suite',
{ tag: [/* TODO: add tags e.g. ...tags.stateful.classic */] },
() => {
test.beforeEach(async ({ browserAuth, apiServices }) => {
// TODO: API-based setup
await browserAuth.loginAsAdmin();
});
test.afterEach(async ({ apiServices }) => {
// TODO: clean up ALL created data
});
test('TODO: test name', async ({ pageObjects, page }) => {
await test.step('TODO: first step', async () => {
// TODO: implement
});
});
}
);
SEQUENTIAL
fi
echo "Created: ${SPEC_FILE}"
echo ""
echo "Next steps:"
echo " 1. Replace all TODO placeholders"
echo " 2. Add page objects if needed (see assets/page_object_template.ts)"
echo " 3. Add API services if needed (see assets/api_service_template.ts)"
echo " 4. Run: node scripts/scout.js update-test-config-manifests"
Related skills
FAQ
What does cypress-to-scout-migration do?
cypress-to-scout-migration skill documents Migrate Kibana Cypress E2E tests (.
When should I use cypress-to-scout-migration?
User asks about cypress-to-scout-migration, migrate kibana cypress e2e tests (.cy.ts) to scout (playwright). applies to any kibana plu.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.