From 8ae9cf4124511978ec2faa6d28b427ddb52980c7 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Thu, 21 Aug 2025 20:13:05 -0400 Subject: [PATCH 01/10] feat(e2e): improve button detection and error handling - Enhanced button detection with multiple fallback strategies - Added automatic force click fallback for UI interactions - Improved modal cleanup and tutorial-specific error messages - Optimized Playwright timeouts and local retry capability --- e2e/playwright.config.ts | 10 +-- e2e/src/pages/AppCatalogPage.ts | 126 +++++++++++++++++++++++++------- e2e/src/pages/BasePage.ts | 40 +++++++++- e2e/tests/foundry.spec.ts | 13 +--- 4 files changed, 145 insertions(+), 44 deletions(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index fa9a5f6..1a2b6b4 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -9,18 +9,18 @@ export default defineConfig({ testDir: './tests', fullyParallel: false, // for more controlled test execution forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 2 : 1, // Allow 1 retry locally for better reliability workers: process.env.CI ? 1 : undefined, - timeout: 60 * 1000, // 60 seconds for entire test + timeout: process.env.CI ? 60 * 1000 : 45 * 1000, // Enhanced timeout hierarchy expect: { - timeout: 10 * 1000, // 10 seconds for assertions + timeout: process.env.CI ? 10 * 1000 : 8 * 1000, // for assertions }, reporter: 'html', use: { testIdAttribute: 'data-test-selector', trace: 'on-first-retry', - actionTimeout: 15 * 1000, // 15 seconds for actions - navigationTimeout: 30 * 1000, // 30 seconds for navigation + actionTimeout: process.env.CI ? 15 * 1000 : 12 * 1000, // Optimized timeouts + navigationTimeout: process.env.CI ? 30 * 1000 : 25 * 1000, // for navigation }, projects: [ diff --git a/e2e/src/pages/AppCatalogPage.ts b/e2e/src/pages/AppCatalogPage.ts index f638433..24769da 100644 --- a/e2e/src/pages/AppCatalogPage.ts +++ b/e2e/src/pages/AppCatalogPage.ts @@ -145,35 +145,27 @@ export class AppCatalogPage extends BasePage { return RetryHandler.withPlaywrightRetry( async () => { - // Check if already installed - const installedStatus = this.page.locator('text=Installed').first(); - if (await this.elementExists(installedStatus, 3000)) { + // Check if already installed using improved detection + const isInstalled = await this.checkInstallationStatus(); + if (isInstalled) { this.logger.info('App is already installed, skipping installation'); return; } - await this.smartClick( - this.page.getByTestId('app-details-page__install-button'), - 'Install button', - { timeout: 15000 } - ); + // Find and click install button using multiple strategies + const installButton = await this.findInstallButton(); + await installButton.click(); await this.waiter.waitForPageLoad(); - await this.smartClick( - this.page.getByTestId('submit'), - 'Submit installation button' - ); + // Find and click submit button using multiple strategies + const submitButton = await this.findSubmitButton(); + await submitButton.click(); await this.waiter.waitForPageLoad(); - // Wait for installation to complete - const statusElement = await this.waiter.waitForVisible( - this.page.getByTestId('status-text'), - { description: 'Installation status', timeout: 10000 } - ); - - await expect(statusElement).toHaveText('Installed', { timeout: 60000 }); + // Wait for installation to complete with improved verification + await this.waitForInstallationComplete(); this.logger.success('App installation completed successfully'); }, @@ -207,17 +199,95 @@ export class AppCatalogPage extends BasePage { ); } + /** + * Check if app is currently installed using multiple detection strategies + */ + private async checkInstallationStatus(): Promise { + const strategies = [ + this.page.getByTestId('status-text').filter({ hasText: /^Installed$/i }), + this.page.getByText('Installed', { exact: true }).first(), + this.page.locator('.installed, [class*="installed"]') + ]; + + for (const strategy of strategies) { + if (await this.elementExists(strategy, 2000)) { + return true; + } + } + return false; + } + + /** + * Find install button using multiple reliable strategies + */ + private async findInstallButton() { + const timeout = config.isCI ? 15000 : 10000; + const strategies = [ + this.page.getByTestId('app-details-page__install-button'), + this.page.getByRole('link', { name: 'Install now' }), + this.page.getByRole('button', { name: 'Install now' }) + ]; + + for (const strategy of strategies) { + if (await this.elementExists(strategy, 3000)) { + this.logger.debug('Found install button using strategy'); + return strategy; + } + } + + throw new Error('Install button not found using any detection strategy'); + } + + /** + * Find submit button using multiple reliable strategies + */ + private async findSubmitButton() { + const strategies = [ + this.page.getByTestId('submit'), + this.page.getByRole('button', { name: 'Save and install' }), + this.page.getByRole('button', { name: /save.*install/i }), + this.page.locator('button[type="submit"]') + ]; + + for (const strategy of strategies) { + if (await this.elementExists(strategy, 5000)) { + this.logger.debug('Found submit button using strategy'); + return strategy; + } + } + + throw new Error('Submit button not found using any detection strategy'); + } + + /** + * Wait for installation to complete with improved verification + */ + private async waitForInstallationComplete(): Promise { + const timeout = config.isCI ? 60000 : 45000; + + // Wait for status element to appear + const statusElement = await this.waiter.waitForVisible( + this.page.getByTestId('status-text'), + { description: 'Installation status', timeout: 10000 } + ); + + // Wait for "Installed" status with extended timeout + await expect(statusElement).toHaveText('Installed', { timeout }); + } + private buildAppNotFoundError(appName: string): string { return [ - `❌ App "${appName}" is not available in the app catalog.\n`, - `This could mean:`, - `1. In LOCAL environment: The app needs to be manually deployed first using the Foundry CLI`, - `2. In CI environment: The app deployment step may have failed\n`, - `To fix this locally:`, - `- Run: foundry app deploy`, - `- Then run: foundry app release`, - `- Make sure your APP_NAME in .env matches your deployed app name\n`, - `Current APP_NAME from .env: ${appName}` + `❌ TUTORIAL SETUP ISSUE: App "${appName}" not found in catalog.\n`, + `📚 For Tutorial Users:`, + `1. Make sure you completed the deployment step: 'foundry apps deploy'`, + `2. Verify the app was released: 'foundry apps release'`, + `3. Check your .env file APP_NAME matches the deployed app\n`, + `🔧 Current Configuration:`, + `- APP_NAME: ${appName}`, + `- Environment: ${config.isCI ? 'CI' : 'Local'}`, + `- Base URL: ${config.falconBaseUrl}\n`, + `💡 Need help? Check the tutorial README for deployment steps.`, + `📖 Tutorial docs: https://github.com/CrowdStrike/foundry-tutorial-quickstart#readme` ].join('\n'); } } \ No newline at end of file diff --git a/e2e/src/pages/BasePage.ts b/e2e/src/pages/BasePage.ts index 5ddde77..0bdcc7d 100644 --- a/e2e/src/pages/BasePage.ts +++ b/e2e/src/pages/BasePage.ts @@ -51,6 +51,7 @@ export abstract class BasePage { /** * Click an element with smart waiting and retry + * Enhanced with automatic force click fallback for complex UI interactions */ protected async smartClick( locator: Locator | string, @@ -72,7 +73,16 @@ export abstract class BasePage { timeout: actualTimeout, description }); - await element.click({ force: options.force, timeout: actualTimeout }); + + try { + // First attempt: normal click + await element.click({ timeout: actualTimeout }); + } catch (error) { + // Second attempt: force click to handle element interception + this.logger.debug(`Normal click failed for ${description}, retrying with force: true`); + await element.click({ force: true, timeout: actualTimeout }); + this.logger.debug(`Force click succeeded for ${description}`); + } }, `Click ${description}` ); @@ -198,6 +208,34 @@ export abstract class BasePage { } } + /** + * Clean up any open modals or dialogs + */ + async cleanupModals(): Promise { + try { + const closeButtons = [ + this.page.getByRole('button', { name: /close|dismiss|cancel/i }), + this.page.locator('[data-testid*="close"], [aria-label*="close"]'), + this.page.locator('.modal-close, [class*="close"]') + ]; + + for (const closeButton of closeButtons) { + if (await this.elementExists(closeButton, 1000)) { + try { + await closeButton.click({ timeout: 2000, force: true }); + await this.page.waitForTimeout(500); + } catch { + // Continue to next strategy + } + } + } + + await this.page.keyboard.press('Escape'); + } catch { + // Modal cleanup should never fail tests + } + } + /** * Abstract method for page-specific verification */ diff --git a/e2e/tests/foundry.spec.ts b/e2e/tests/foundry.spec.ts index 9d2095b..7a98a02 100644 --- a/e2e/tests/foundry.spec.ts +++ b/e2e/tests/foundry.spec.ts @@ -25,7 +25,7 @@ test.describe('Foundry Tutorial Quickstart E2E Tests', () => { }); // Clean up after each test - test.afterEach(async ({ page }, testInfo) => { + test.afterEach(async ({ page, appCatalogPage }, testInfo) => { // Take screenshot on failure for debugging if (testInfo.status !== testInfo.expectedStatus) { const screenshotPath = `test-failure-${testInfo.title.replace(/\s+/g, '-').toLowerCase()}-${Date.now()}.png`; @@ -41,15 +41,8 @@ test.describe('Foundry Tutorial Quickstart E2E Tests', () => { logger.success(`Test passed: ${testInfo.title}`, { duration: testInfo.duration }); } - // Clear any lingering modals or dialogs - try { - const modalCloseButton = page.getByRole('button', { name: /close|dismiss|cancel/i }); - if (await modalCloseButton.isVisible({ timeout: 1000 })) { - await modalCloseButton.click({ timeout: 2000 }); - } - } catch { - // Ignore if no modals to close - } + // Enhanced modal cleanup + await appCatalogPage.cleanupModals(); }); test.describe('App Installation and Basic Navigation', () => { From cfbe4f7ae09587b486318039a2849f7736fb9005 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Thu, 21 Aug 2025 20:24:52 -0400 Subject: [PATCH 02/10] simplify: remove over-engineered button detection strategies The CI passed with the simple TestId approach, confirming that multiple fallback strategies were unnecessary for the tutorial environment. Reverted to the original clean approach that was already working. --- e2e/src/pages/AppCatalogPage.ts | 106 ++++++-------------------------- 1 file changed, 19 insertions(+), 87 deletions(-) diff --git a/e2e/src/pages/AppCatalogPage.ts b/e2e/src/pages/AppCatalogPage.ts index 24769da..ad011c5 100644 --- a/e2e/src/pages/AppCatalogPage.ts +++ b/e2e/src/pages/AppCatalogPage.ts @@ -145,27 +145,35 @@ export class AppCatalogPage extends BasePage { return RetryHandler.withPlaywrightRetry( async () => { - // Check if already installed using improved detection - const isInstalled = await this.checkInstallationStatus(); - if (isInstalled) { + // Check if already installed + const installedStatus = this.page.locator('text=Installed').first(); + if (await this.elementExists(installedStatus, 3000)) { this.logger.info('App is already installed, skipping installation'); return; } - // Find and click install button using multiple strategies - const installButton = await this.findInstallButton(); - await installButton.click(); + await this.smartClick( + this.page.getByTestId('app-details-page__install-button'), + 'Install button', + { timeout: 15000 } + ); await this.waiter.waitForPageLoad(); - // Find and click submit button using multiple strategies - const submitButton = await this.findSubmitButton(); - await submitButton.click(); + await this.smartClick( + this.page.getByTestId('submit'), + 'Submit installation button' + ); await this.waiter.waitForPageLoad(); - // Wait for installation to complete with improved verification - await this.waitForInstallationComplete(); + // Wait for installation to complete + const statusElement = await this.waiter.waitForVisible( + this.page.getByTestId('status-text'), + { description: 'Installation status', timeout: 10000 } + ); + + await expect(statusElement).toHaveText('Installed', { timeout: 60000 }); this.logger.success('App installation completed successfully'); }, @@ -199,82 +207,6 @@ export class AppCatalogPage extends BasePage { ); } - /** - * Check if app is currently installed using multiple detection strategies - */ - private async checkInstallationStatus(): Promise { - const strategies = [ - this.page.getByTestId('status-text').filter({ hasText: /^Installed$/i }), - this.page.getByText('Installed', { exact: true }).first(), - this.page.locator('.installed, [class*="installed"]') - ]; - - for (const strategy of strategies) { - if (await this.elementExists(strategy, 2000)) { - return true; - } - } - return false; - } - - /** - * Find install button using multiple reliable strategies - */ - private async findInstallButton() { - const timeout = config.isCI ? 15000 : 10000; - const strategies = [ - this.page.getByTestId('app-details-page__install-button'), - this.page.getByRole('link', { name: 'Install now' }), - this.page.getByRole('button', { name: 'Install now' }) - ]; - - for (const strategy of strategies) { - if (await this.elementExists(strategy, 3000)) { - this.logger.debug('Found install button using strategy'); - return strategy; - } - } - - throw new Error('Install button not found using any detection strategy'); - } - - /** - * Find submit button using multiple reliable strategies - */ - private async findSubmitButton() { - const strategies = [ - this.page.getByTestId('submit'), - this.page.getByRole('button', { name: 'Save and install' }), - this.page.getByRole('button', { name: /save.*install/i }), - this.page.locator('button[type="submit"]') - ]; - - for (const strategy of strategies) { - if (await this.elementExists(strategy, 5000)) { - this.logger.debug('Found submit button using strategy'); - return strategy; - } - } - - throw new Error('Submit button not found using any detection strategy'); - } - - /** - * Wait for installation to complete with improved verification - */ - private async waitForInstallationComplete(): Promise { - const timeout = config.isCI ? 60000 : 45000; - - // Wait for status element to appear - const statusElement = await this.waiter.waitForVisible( - this.page.getByTestId('status-text'), - { description: 'Installation status', timeout: 10000 } - ); - - // Wait for "Installed" status with extended timeout - await expect(statusElement).toHaveText('Installed', { timeout }); - } - private buildAppNotFoundError(appName: string): string { return [ `❌ TUTORIAL SETUP ISSUE: App "${appName}" not found in catalog.\n`, From 5df5866802797f30d6954ba80a5838e500036fd1 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Thu, 21 Aug 2025 20:28:48 -0400 Subject: [PATCH 03/10] simplify: remove tutorial-specific emojis and verbose messaging Error messages should be practical for actual users running tests locally, not overly tutorial-focused. Reverted to cleaner, more standard format. --- e2e/src/pages/AppCatalogPage.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/e2e/src/pages/AppCatalogPage.ts b/e2e/src/pages/AppCatalogPage.ts index ad011c5..501d61d 100644 --- a/e2e/src/pages/AppCatalogPage.ts +++ b/e2e/src/pages/AppCatalogPage.ts @@ -209,17 +209,15 @@ export class AppCatalogPage extends BasePage { private buildAppNotFoundError(appName: string): string { return [ - `❌ TUTORIAL SETUP ISSUE: App "${appName}" not found in catalog.\n`, - `📚 For Tutorial Users:`, - `1. Make sure you completed the deployment step: 'foundry apps deploy'`, - `2. Verify the app was released: 'foundry apps release'`, - `3. Check your .env file APP_NAME matches the deployed app\n`, - `🔧 Current Configuration:`, - `- APP_NAME: ${appName}`, - `- Environment: ${config.isCI ? 'CI' : 'Local'}`, - `- Base URL: ${config.falconBaseUrl}\n`, - `💡 Need help? Check the tutorial README for deployment steps.`, - `📖 Tutorial docs: https://github.com/CrowdStrike/foundry-tutorial-quickstart#readme` + `❌ App "${appName}" is not available in the app catalog.\n`, + `This could mean:`, + `1. In LOCAL environment: The app needs to be manually deployed first using the Foundry CLI`, + `2. In CI environment: The app deployment step may have failed\n`, + `To fix this locally:`, + `- Run: foundry apps deploy`, + `- Then run: foundry apps release`, + `- Make sure your APP_NAME in .env matches your deployed app name\n`, + `Current APP_NAME from .env: ${appName}` ].join('\n'); } } \ No newline at end of file From afce3a573c634e561ae2d88be38c2d1c7d658c32 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Mon, 25 Aug 2025 20:26:49 -0600 Subject: [PATCH 04/10] Use quiet config for dotenvx --- e2e/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 1a2b6b4..03cace5 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; import { AuthFile } from './constants/AuthFile'; if (!process.env.CI) { - require("dotenv").config({ path: ".env" }); + require("dotenv").config({ path: ".env", quiet: true }); } export default defineConfig({ From a85a5c09c571932b494b66aa839c50f75eb2f0b9 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Mon, 25 Aug 2025 20:32:28 -0600 Subject: [PATCH 05/10] Run dotenvx in quiet mode --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 66e1f81..f7f9536 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,7 +97,7 @@ jobs: directory: e2e - name: Run Playwright tests - run: npx dotenvx run -- npx playwright test + run: npx dotenvx run --quiet -- npx playwright test working-directory: e2e - name: Upload Playwright report From 37670683292f70e9d95a77a7d34e9ee334d6da4e Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Mon, 25 Aug 2025 20:58:34 -0600 Subject: [PATCH 06/10] fix(e2e): reduce verbose logging in CI environment Align Logger.ts with foundry-sample-mitre implementation for quieter CI output: - Remove verbose JSON logging in CI environments - Only log errors, warnings (selective), and final test results in CI - Suppress step-level logs completely in CI - Maintain full verbose logging for local development This resolves excessive log output during CI test runs while preserving detailed debugging information for local development. --- e2e/src/utils/Logger.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/e2e/src/utils/Logger.ts b/e2e/src/utils/Logger.ts index 27f00e4..02271dd 100644 --- a/e2e/src/utils/Logger.ts +++ b/e2e/src/utils/Logger.ts @@ -146,16 +146,21 @@ export class Logger { private log(level: LogLevel, message: string, context: LogContext = {}): void { const timestamp = new Date().toISOString(); - const logEntry = { - timestamp, - level, - message, - ...context - }; - // In CI, use structured JSON logging for better parsing - if (this.isCI && level !== 'step') { - console.log(JSON.stringify(logEntry)); + // In CI, be much less verbose with plain text output + if (this.isCI) { + // Only log errors, warnings, and final test results in CI + if (level === 'error' || + (level === 'warn' && !message.includes('App page loaded but no content detected')) || + (level === 'info' && ( + message.includes('✅ Test passed') || + message.includes('❌ Test failed') || + message.includes('E2E Test Config:') + ))) { + // Use plain text in CI for better readability + console.log(message); + } + // Completely suppress 'step' level in CI } else { // In local development, use human-readable format console.log(message); From eaa8eb1a653c18222e23e61e3b4b7e59363168a6 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Mon, 25 Aug 2025 21:05:25 -0600 Subject: [PATCH 07/10] fix(e2e): remove duplicate Test Configuration logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove redundant config.logSummary() call from foundry.spec.ts since the fixtures.ts already logs configuration once per test run with proper guarding via CONFIG_LOGGED environment variable. This eliminates the duplicate "🔧 Test Configuration:" output in CI logs. --- e2e/tests/foundry.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/tests/foundry.spec.ts b/e2e/tests/foundry.spec.ts index 7a98a02..094cd5c 100644 --- a/e2e/tests/foundry.spec.ts +++ b/e2e/tests/foundry.spec.ts @@ -13,7 +13,6 @@ test.describe('Foundry Tutorial Quickstart E2E Tests', () => { // Global setup for the entire test suite test.beforeAll(async () => { - config.logSummary(); logger.info('Starting Foundry Tutorial Quickstart E2E test suite'); // Log test environment info From 646d25b9bfa7cfced0791b7e9906119f2e79e3ea Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Mon, 25 Aug 2025 21:10:41 -0600 Subject: [PATCH 08/10] fix(e2e): ensure screenshot directory exists before taking screenshots Align screenshot implementation with foundry-sample-mitre: - Add directory creation logic to ensure test-results directory exists - Use path.join() for proper cross-platform path handling - Remove invalid quality parameter for PNG screenshots - Improve error messaging with detailed error information This resolves "Failed to take screenshot" errors in CI where the test-results directory doesn't exist by default. --- e2e/src/config/TestConfig.ts | 4 ++-- e2e/src/pages/BasePage.ts | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/e2e/src/config/TestConfig.ts b/e2e/src/config/TestConfig.ts index d2ce293..f573c2c 100644 --- a/e2e/src/config/TestConfig.ts +++ b/e2e/src/config/TestConfig.ts @@ -107,8 +107,8 @@ export class TestConfig { return { path: this.screenshotPath, fullPage: true, - type: 'png' as const, - quality: this.isCI ? 80 : 100 + type: 'png' as const + // Note: quality parameter is not supported for PNG screenshots }; } diff --git a/e2e/src/pages/BasePage.ts b/e2e/src/pages/BasePage.ts index 0bdcc7d..8a0347e 100644 --- a/e2e/src/pages/BasePage.ts +++ b/e2e/src/pages/BasePage.ts @@ -127,11 +127,22 @@ export abstract class BasePage { protected async takeScreenshot(filename: string, context: LogContext = {}): Promise { try { const screenshotConfig = config.getScreenshotConfig(); - const fullPath = `${screenshotConfig.path}/${filename}`; + + // Ensure the directory exists + const fs = require('fs'); + const path = require('path'); + const screenshotDir = screenshotConfig.path; + if (!fs.existsSync(screenshotDir)) { + fs.mkdirSync(screenshotDir, { recursive: true }); + } + + // Create full path for the screenshot file + const fullPath = path.join(screenshotDir, filename); await this.page.screenshot({ path: fullPath, - ...screenshotConfig + fullPage: screenshotConfig.fullPage, + type: screenshotConfig.type }); this.logger.debug(`Screenshot saved: ${filename}`, { @@ -139,7 +150,8 @@ export abstract class BasePage { path: fullPath }); } catch (error) { - this.logger.warn(`Failed to take screenshot: ${filename}`, error instanceof Error ? error : undefined, context); + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.warn(`Failed to take screenshot: ${filename} - ${errorMessage}`, error instanceof Error ? error : undefined, context); } } From 32d2a18e7d05fcd66256f0bc4a718d66ead8aa74 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Wed, 17 Sep 2025 22:04:38 -0700 Subject: [PATCH 09/10] Add Playwright screenshot and video capture on test failures - Update main.yml workflow to upload test-results/ directory as artifacts - Configure Playwright to capture screenshots only on failure - Configure Playwright to retain videos only on failure - Provides better debugging capabilities for failed E2E tests --- .github/workflows/main.yml | 8 ++++++++ e2e/playwright.config.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7f9536..5bd3bdc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -108,6 +108,14 @@ jobs: path: e2e/playwright-report/ retention-days: 30 + - name: Upload test results and screenshots + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-test-results-${{ env.APP_NAME }} + path: e2e/test-results/ + retention-days: 30 + - name: Delete app from Falcon if: always() run: | diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 03cace5..1a59bbe 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -19,6 +19,8 @@ export default defineConfig({ use: { testIdAttribute: 'data-test-selector', trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', actionTimeout: process.env.CI ? 15 * 1000 : 12 * 1000, // Optimized timeouts navigationTimeout: process.env.CI ? 30 * 1000 : 25 * 1000, // for navigation }, From 797be5ae8512d7217cfedd8e877726d47d1b62c1 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Mon, 22 Sep 2025 05:47:10 -0600 Subject: [PATCH 10/10] Remove dependency ranges from package.json and regenerate lock files --- e2e/package-lock.json | 11 +++++++---- e2e/package.json | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6d66718..0b731d3 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -9,12 +9,15 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@dotenvx/dotenvx": "^1.47.3", - "otpauth": "^9.4.0" + "@dotenvx/dotenvx": "1.47.3", + "otpauth": "9.4.0" }, "devDependencies": { - "@playwright/test": "^1.53.2", - "@types/node": "^24.0.10" + "@playwright/test": "1.53.2", + "@types/node": "24.0.10" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@dotenvx/dotenvx": { diff --git a/e2e/package.json b/e2e/package.json index a432b9c..838d87c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -14,11 +14,11 @@ "node": ">=22.0.0" }, "dependencies": { - "@dotenvx/dotenvx": "^1.47.3", - "otpauth": "^9.4.0" + "@dotenvx/dotenvx": "1.47.3", + "otpauth": "9.4.0" }, "devDependencies": { - "@playwright/test": "^1.53.2", - "@types/node": "^24.0.10" + "@playwright/test": "1.53.2", + "@types/node": "24.0.10" } }