agentleFS
CrowdStrikeCrowdStrike/foundry-skills/skills/e2e-testing/SKILL.md

e2e-testing

Skill35 starsChanged 3 days agoMITView on GitHub

End-to-end testing for Falcon Foundry apps using Playwright and @crowdstrike/foundry-playwright. TRIGGER when user asks to "add e2e tests", "add playwright tests", "write end-to-end tests", "test my app", or mentions "e2e", "playwright", or "end-to-end" in the context of testing a Foundry app. DO NOT TRIGGER during normal app creation, UI development, or function development. This skill is opt-in; not all apps need e2e tests.

  • Reads credentials
  • Installs packages

We split this skill into the rules it gives an agent

8 rules. 0 are common, 7 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 38 edited versions we read keep these.

  • 7

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    chromium: run your tests

    line 89
  2. Very specific:

    Create src/fixtures.ts to wire up page objects as Playwright fixtures.

    line 129
  3. Distinctive:

    If your app has API integration settings during install (e.g., ServiceNow credentials), the default install will fail because the Install …

    line 196
  4. Distinctive:

    How to discover field names: Use Playwright MCP to take a snapshot of the install page and inspect the form fields.

    line 249
  5. Distinctive:

    Add the custom page to your fixtures alongside library page objects.

    line 359
  6. Distinctive:

    Key principle: When using Playwright MCP interactively, authentication is manual.

    line 365
  7. Distinctive:

    Concurrency serialization: Only one e2e run per repo at a time (prevents deployment collisions)

    line 375
  8. Distinctive:

    debugging-workflows: Use for deploy/release issues that block testing

    line 465

“Common” is measured: kept by at least 6 in 10 of the 38 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Foundry E2E Testing

End-to-end testing for Falcon Foundry apps using Playwright and the @crowdstrike/foundry-playwright library.

The library provides authentication, app install/uninstall, page objects, and configuration so each app only writes its app-specific tests.

Part of a suite. If development-workflow has not already run, and this is a new app or its first capability, load the development-workflow skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

Quick Start

1. Create the e2e/ directory

my-foundry-app/
├── e2e/
│   ├── .env                    # Local credentials (git-ignored)
│   ├── .env.sample             # Template for other developers
│   ├── .gitignore
│   ├── package.json
│   ├── playwright.config.ts
│   └── tests/
│       └── foundry.spec.ts
├── manifest.yml
└── ...

2. package.json

Node.js LTS is recommended.

{
  "name": "playwright-foundry",
  "version": "1.0.0",
  "scripts": {
    "test": "npx playwright test",
    "test:ui": "npx playwright test --ui",
    "test:debug": "npx playwright test --debug",
    "test:verbose": "DEBUG=true npx playwright test --reporter=list"
  },
  "type": "commonjs",
  "devDependencies": {
    "@crowdstrike/foundry-playwright": "0.5.6",
    "@types/node": "25.6.0"
  }
}

Always pin exact versions — never use "latest", "^", or "~". Check npm for the current version of each package.

The library brings @playwright/test, @dotenvx/dotenvx, and otpauth as transitive dependencies; don't install them separately.

3. .env

FALCON_USERNAME=your.email@company.com
FALCON_PASSWORD=your-password
FALCON_AUTH_SECRET=your-totp-secret
FALCON_BASE_URL=https://falcon.us-2.crowdstrike.com
APP_NAME=your-app-name

Convention for sample apps: Set APP_NAME to match the manifest name field, which should match the repo name (e.g., foundry-sample-functions-python). This avoids spaces in names and simplifies CI. This is a convention, not a hard requirement.

4. playwright.config.ts

import { defineFoundryConfig } from '@crowdstrike/foundry-playwright';

export default defineFoundryConfig();

This gives you the standard 4-project pipeline automatically:

  1. setup: authenticate and save session state
  2. app-install: install the app via App Catalog
  3. chromium: run your tests
  4. app-uninstall: clean up after tests

5. .gitignore

node_modules/
playwright/.auth/
playwright-report/
test-results/
.env

6. Install and run

cd e2e
npm install
npx playwright install chromium --with-deps
npm test

Writing Tests

Available page objects

The library provides these page objects:

ClassPurpose
WorkflowsPageSearch, open, execute, and verify Falcon Fusion SOAR workflows
DetectionExtensionPageNavigate to Endpoint Detections, expand extensions, return iframe FrameLocator
HostManagementPageNavigate to host management, retrieve host IDs
AppCatalogPageInstall, uninstall, and navigate to apps
AppBuilderPageDisable workflow provisioning before install
AppManagerPageFind and navigate to apps in App Manager
FoundryHomePageNavigate to Falcon Foundry home

Fixtures pattern

Create src/fixtures.ts to wire up page objects as Playwright fixtures. Only import what your tests actually use — don't define unused fixtures:

import { test as baseTest } from '@playwright/test';
import { DetectionExtensionPage, WorkflowsPage, HostManagementPage } from '@crowdstrike/foundry-playwright';

type FoundryFixtures = {
  detectionExtensionPage: DetectionExtensionPage;
  workflowsPage: WorkflowsPage;
  hostManagementPage: HostManagementPage;
};

export const test = baseTest.extend<FoundryFixtures>({
  detectionExtensionPage: async ({ page }, use) => { await use(new DetectionExtensionPage(page)); },
  workflowsPage: async ({ page }, use) => { await use(new WorkflowsPage(page)); },
  hostManagementPage: async ({ page }, use) => { await use(new HostManagementPage(page)); },
});

export { expect } from '@playwright/test';

Playwright fixtures are lazy, so unused ones cost nothing at runtime, but they're dead code. Add fixtures as tests need them.

Example test: workflows

import { test } from '../src/fixtures';

test.describe.configure({ mode: 'serial' });

test('should execute workflow', async ({ workflowsPage }) => {
  test.setTimeout(180000);
  await workflowsPage.navigateToWorkflows();
  await workflowsPage.executeAndVerifyWorkflow('My Workflow Name');
  await workflowsPage.verifyWorkflowExecutionCompleted();
});

test('should execute workflow with input', async ({ workflowsPage, hostManagementPage }) => {
  test.setTimeout(180000);
  const hostId = await hostManagementPage.getFirstHostId();
  if (!hostId) { test.skip(true, 'No hosts available'); return; }

  await workflowsPage.navigateToWorkflows();
  await workflowsPage.executeAndVerifyWorkflow('Host Details Workflow', {
    inputs: { 'Host ID': hostId },
  });
  await workflowsPage.verifyWorkflowExecutionCompleted();
});

executeAndVerifyWorkflow() handles search, execution trigger, and initial verification. verifyWorkflowExecutionCompleted() opens the execution detail view in a new tab and polls until the status leaves "In Progress" — it fails the test if the execution reports "Failed" and times out after 120s by default. For render-only checks (a workflow without credentials), use verifyWorkflowRenders().

Example test: UI extensions

import { test, expect } from '../src/fixtures';

test('should render extension', async ({ detectionExtensionPage }) => {
  const frame = await detectionExtensionPage.openExtension('hello');
  await expect(frame.getByText(/My App Title/i)).toBeVisible({ timeout: 10000 });
});

openExtension() navigates to Endpoint Detections, opens the first detection, scrolls to the named extension button, expands it, and returns the iframe FrameLocator.

Apps with Configuration Screens

If your app has API integration settings during install (e.g., ServiceNow credentials), the default install will fail because the Install button stays disabled until fields are filled.

1. Add integration credentials to .env and .env.sample

# .env.sample — commit this as a template
SERVICENOW_INSTANCE_URL=https://dev123456.service-now.com
SERVICENOW_USERNAME=your-servicenow-username
SERVICENOW_PASSWORD=your-servicenow-password

# .env — local values, git-ignored
SERVICENOW_INSTANCE_URL=https://dev99999.service-now.com
SERVICENOW_USERNAME=admin
SERVICENOW_PASSWORD=s3cret

2. Create a custom tests/app-install.setup.ts

import { test as setup } from '@playwright/test';
import { AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

setup('install app', async ({ page }) => {
  const catalog = new AppCatalogPage(page);

  const instanceUrl = process.env.SERVICENOW_INSTANCE_URL;
  const username = process.env.SERVICENOW_USERNAME;
  const password = process.env.SERVICENOW_PASSWORD;
  if (!instanceUrl || !username || !password) {
    throw new Error('Missing required ServiceNow env vars: SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, SERVICENOW_PASSWORD');
  }

  await catalog.installApp(config.appName, {
    configureSettings: async (page) => {
      await page.getByRole('textbox', { name: 'Name', exact: true }).fill('ServiceNow Integration');
      await page.getByRole('textbox', { name: 'Instance' }).fill(instanceUrl);
      await page.getByRole('textbox', { name: 'Username' }).fill(username);
      await page.getByRole('textbox', { name: 'Password' }).fill(password);
    },
  });
});

The library loads .env automatically (via @dotenvx/dotenvx) so process.env values are available without extra setup. In CI, set these as GitHub Actions secrets instead.

3. Point the config at the custom install

export default defineFoundryConfig({
  appInstallDir: './tests',
});

How to discover field names: Use Playwright MCP to take a snapshot of the install page and inspect the form fields. See debugging-with-mcp.md.

Disabling Workflow Provisioning

Apps with workflows that require valid API credentials will fail during install if you provide fake credentials for the configuration screen. Some workflows are also long-running (e.g., Anomali ThreatStream ingestion) and shouldn't be provisioned during testing because they'll start automatically. Use AppBuilderPage.disableWorkflowProvisioning() before install to skip provisioning:

import { test as setup } from '@playwright/test';
import { AppBuilderPage, AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

setup('install app', async ({ page }) => {
  setup.setTimeout(300000);
  const appBuilder = new AppBuilderPage(page);
  await appBuilder.disableWorkflowProvisioning(config.appName);

  const catalog = new AppCatalogPage(page);
  await catalog.installApp(config.appName);
});

This navigates to the App Builder, finds the app, toggles off "Provision on install" for each workflow template, and saves. When it changed anything, it also deploys and releases a new version of the app from the App Builder; it skips all of that if a previous release already has provisioning disabled. Call it before installApp() in your custom app-install.setup.ts.

Multi-Screen Configuration Wizards

Some apps have settings spread across multiple screens (e.g., Workday and SailPoint, with 3 each). The screens don't always appear in the same order, so loop over them, detect the current screen by a field only it has, fill it, and click "Next setting" when it's visible:

import { test as setup } from '@playwright/test';
import { AppBuilderPage, AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

setup('install app', async ({ page }) => {
  setup.setTimeout(300000);
  const appBuilder = new AppBuilderPage(page);
  await appBuilder.disableWorkflowProvisioning(config.appName);

  const catalog = new AppCatalogPage(page);

  await catalog.installApp(config.appName, {
    configureSettings: async (page) => {
      const nextButton = page.getByRole('button', { name: 'Next setting' });

      for (let screen = 0; screen < 3; screen++) {
        if (await page.getByRole('textbox', { name: 'Client ID' }).isVisible({ timeout: 3000 }).catch(() => false)) {
          await page.getByRole('textbox', { name: 'Name' }).fill('My Integration');
          await page.getByRole('textbox', { name: 'Host' }).fill('https://api.example.com');
          await page.getByRole('textbox', { name: 'Client ID' }).fill(process.env.CLIENT_ID!);
          await page.getByRole('textbox', { name: 'Client Secret' }).fill(process.env.CLIENT_SECRET!);
        } else if (await page.getByRole('textbox', { name: 'Tenant ID' }).isVisible({ timeout: 3000 }).catch(() => false)) {
          await page.getByRole('textbox', { name: 'Tenant ID' }).fill(process.env.TENANT_ID!);
        } else {
          await page.getByRole('textbox', { name: 'Name' }).fill('My Second Integration');
          await page.getByRole('textbox', { name: 'Host' }).fill('https://api.example.com');
        }

        // The last screen shows "Install app" instead of "Next setting"
        if (await nextButton.isVisible({ timeout: 2000 }).catch(() => false)) {
          await nextButton.click();
          await page.waitForLoadState('domcontentloaded').catch(() => {});
        }
      }
    },
  });
});

Set the loop count to your app's screen count. The .catch() calls keep a missing field or an already-loaded page from failing setup.

Custom Page Objects

For app-specific UI that the library doesn't cover, extend BasePage:

import { Page, expect } from '@playwright/test';
import { BasePage, AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

export class MyAppPage extends BasePage {
  constructor(page: Page) {
    super(page, 'MyAppPage'); // display name for logging only
  }

  protected getPagePath(): string {
    throw new Error('Direct path navigation not supported. Use navigateToInstalledApp() instead.');
  }

  protected async verifyPageLoaded(): Promise<void> {
    await this.page.locator('h1').filter({ hasText: /My App/i }).waitFor();
  }

  async navigateToInstalledApp(): Promise<void> {
    return this.withTiming(async () => {
      const catalog = new AppCatalogPage(this.page);
      await catalog.navigateToInstalledApp(config.appName);
      await this.verifyPageLoaded();
    }, 'Navigate to installed app');
  }

  async verifyAppContent(): Promise<void> {
    return this.withTiming(async () => {
      const iframe = this.page.frameLocator('iframe[name="portal"]');
      const heading = iframe.getByRole('heading', { name: /My App/i });
      await expect(heading).toBeVisible({ timeout: 10000 });
    }, 'Verify app content');
  }
}

Foundry app page URLs contain deployment-specific IDs that change on every deploy, so never hardcode paths. Use AppCatalogPage.navigateToInstalledApp() to navigate via the App Catalog menu instead.

BasePage gives you smartClick(), withTiming(), navigateToPath(), elementExists(), a logger, and a waiter (SmartWaiter instance).

Add the custom page to your fixtures alongside library page objects.

Debugging with Playwright MCP

Playwright MCP is invaluable for writing and debugging e2e tests. See references/debugging-with-mcp.md for detailed patterns.

Key principle: When using Playwright MCP interactively, authentication is manual. Navigate to the Falcon login page, then tell the user: "Please log in to Falcon in the browser, then let me know when you're ready." Do not attempt automated TOTP login through MCP.

CI with GitHub Actions

The sample apps use a shared e2e.yml workflow pattern. Rather than duplicating it here (where it would go stale), reference the canonical implementation:

Reference: foundry-sample-functions-python/.github/workflows/e2e.yml

Key CI concepts

  1. Concurrency serialization: Only one e2e run per repo at a time (prevents deployment collisions)
  2. Unique app names: CI generates a unique name from repo + actor + timestamp to avoid conflicts
  3. Manifest ID stripping: yq -i 'del(.. | select(has("id")).id) | del(.. | select(has("app_id")).app_id)' manifest.yml
  4. Deploy → wait → release → test → cleanup: The workflow deploys, polls for success, releases, runs tests, then always deletes the app
  5. App cleanup: foundry apps delete -f runs in an always() step so apps are cleaned up even on failure

Required GitHub secrets

SecretPurpose
FOUNDRY_API_CLIENT_IDFoundry CLI authentication
FOUNDRY_API_CLIENT_SECRETFoundry CLI authentication
FOUNDRY_CIDCrowdStrike Customer ID
FOUNDRY_CLOUD_REGIONCloud region (us-1, us-2, eu-1, us-gov-1, us-gov-2)
FALCON_USERNAMEFalcon console login for Playwright
FALCON_PASSWORDFalcon console password
FALCON_AUTH_SECRETTOTP secret for 2FA

CI-specific behavior

The library detects CI=true and adjusts:

  • Higher timeouts (60s default vs 45s local)
  • Retries enabled (2 retries vs 0 local)
  • .env loading skipped (credentials come from environment)

defineFoundryConfig() Options

OptionTypeDefault
testDirstring'./tests'
appInstallDirstringLibrary built-in (override for apps with config screens)
timeoutnumber60s (CI) / 45s (local)
retriesnumber2 (CI) / 0 (local)
reporterstring'list'
useobjectMerged with defaults (testIdAttribute: 'data-test-selector')
projectsarrayReplaces the default 4-project pipeline if provided

Sidebar navigation flakiness

The Falcon sidebar menu re-renders during navigation, which can detach DOM elements mid-click and cause intermittent timeouts. This affects any test that navigates through the sidebar (detections, workflows, host management, etc.). The library's navigation methods include retry logic for this, but with retries: 0 locally, a single re-render failure will fail the test.

For apps that navigate through the sidebar, set retries: 2 to handle this reliably:

export default defineFoundryConfig({ retries: 2 });

Common Pitfalls

ProblemCauseFix
"Could not find app in catalog"APP_NAME doesn't match the manifest nameEnsure .env APP_NAME matches manifest.yml name field
Install button stays disabledApp has config screens (API integrations)Create custom app-install.setup.ts with configureSettings()
"collection existed previously but was deleted"Stale IDs in manifest from a previous deploymentStrip all IDs: yq -i 'del(.. | select(has("id")).id) | del(.. | select(has("app_id")).app_id)' manifest.yml
Tests fail on first run after deployRelease not propagated yetWait 15-30s after release before running tests; CI workflow includes a sleep
Tests pass locally but fail in CIDifferent timeout/retry settingsCheck CI=true is set; library auto-adjusts timeouts
Login fails with account lockoutRunning npm test in multiple apps simultaneouslyRun tests for one app at a time. Concurrent login attempts against the same Falcon account trigger rate-limiting and temporarily lock the account.

Reference Implementations

All CrowdStrike/foundry-sample-* apps have e2e tests using this library:

AppHighlights
foundry-sample-functions-pythonconfigureSettings(), workflows, extension, host details
foundry-sample-mitreCustom page objects (MitreChartPage), parallel tests
foundry-sample-anomali-threatstreamAPI integration config
foundry-sample-category-blockingUI page testing
foundry-sample-charlotte-toolkitCharlotte AI toolkit
foundry-sample-collections-toolkitCollection CRUD
foundry-sample-detection-translationDetection extension
foundry-sample-foundryjs-demoFoundry-JS demo
foundry-sample-idp-notificationsIDP notifications
foundry-sample-insider-risk-sailpointSailPoint integration
foundry-sample-insider-risk-workdayWorkday integration
foundry-sample-logscaleLogScale data ingestion
foundry-sample-ngsiem-importerNext-Gen SIEM importer
foundry-sample-openrouter-toolkitOpenRouter AI toolkit
foundry-sample-rapid-responseRapid response
foundry-sample-scalable-rtrScalable RTR
foundry-sample-servicenow-idpServiceNow IDP
foundry-sample-servicenow-itsmServiceNow ITSM
foundry-sample-threat-intelThreat intelligence
foundry-sample-zscaler-internet-accessZscaler integration

Integration with Other Skills

  • development-workflow: E2E testing is delegated from the orchestrator when users request it
  • ui-development: Tests verify UI pages and extensions render correctly in the Falcon console
  • workflows-development: Tests verify workflow execution and completion
  • debugging-workflows: Use for deploy/release issues that block testing

Download now (ZIP)

  • Other versions89 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 89

    1. affaan-m/ECC★ 246ksame name
    2. langflow-ai/langflow★ 156ksame name
    3. All-Hands-AI/OpenHands★ 92ksame name
    4. OpenHands/OpenHands★ 92ksame name
    5. pulsejet/memories★ 3.9ksame name
    6. xu-xiang/everything-claude-code-zh★ 1.9ksame name
    7. paradigmxyz/centaur★ 1.4ksame name
    8. etewiah/property_web_builder★ 644same name
    9. nodetool-ai/nodetool★ 603same name
    10. Opentrons/opentrons★ 529same name

    79 more not listed

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • Used alongsideProjects with it also use everything-claude-code, investor-materials and 10 more.

    Other files people use alongside this one.

    Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
    • InfopibeSkill

      everything-claude-code

      Also in 10 projects that use this

    • InfopibeSkill

      investor-materials

      Create and update pitch decks, one-pagers, investor memos, accelerator applications, financial models, and fundraising materials. Use when the user needs investor-facing documents, projections, use-of-funds tables, milestone plans, or materials that must stay internally consistent across multiple fundraising assets.

      Also in 28 projects that use this

    • mryeastcodeSkill

      everything-claude-code-conventions

      Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

      Also in 8 projects that use this

    • tpavanipradeepSkill

      bun-runtime

      Bun as runtime, package manager, bundler, and test runner. When to choose Bun vs Node, migration notes, and Vercel support.

      Also in 18 projects that use this

    • niloykumarbarmanSkill

      agent-sort

      Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

      Also in 8 projects that use this

    • niloykumarbarmanSkill

      product-capability

      Translate PRD intent, roadmap asks, or product discussions into an implementation-ready capability plan that exposes constraints, invariants, interfaces, and unresolved decisions before multi-service work starts. Use when the user needs an ECC-native PRD-to-SRS lane instead of vague planning prose.

      Also in 9 projects that use this

    • +1 more from Infopibe
  • In this repository16 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.