Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
11 changes: 7 additions & 4 deletions e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
14 changes: 8 additions & 6 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@ 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({
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
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
},

projects: [
Expand Down
4 changes: 2 additions & 2 deletions e2e/src/config/TestConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down
4 changes: 2 additions & 2 deletions e2e/src/pages/AppCatalogPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ export class AppCatalogPage extends BasePage {
`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`,
`- 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');
Expand Down
58 changes: 54 additions & 4 deletions e2e/src/pages/BasePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}`
);
Expand Down Expand Up @@ -117,19 +127,31 @@ export abstract class BasePage {
protected async takeScreenshot(filename: string, context: LogContext = {}): Promise<void> {
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}`, {
...context,
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);
}
}

Expand Down Expand Up @@ -198,6 +220,34 @@ export abstract class BasePage {
}
}

/**
* Clean up any open modals or dialogs
*/
async cleanupModals(): Promise<void> {
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
*/
Expand Down
23 changes: 14 additions & 9 deletions e2e/src/utils/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 3 additions & 11 deletions e2e/tests/foundry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,7 +24,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`;
Expand All @@ -41,15 +40,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', () => {
Expand Down
Loading