diff --git a/.github/workflows/deploy-and-test.yml b/.github/workflows/deploy-and-test.yml index 4d4251022..db8856bda 100644 --- a/.github/workflows/deploy-and-test.yml +++ b/.github/workflows/deploy-and-test.yml @@ -10,9 +10,6 @@ on: # Any needed permissions should be configured at the job level. permissions: {} -env: - CYPRESS_TEST_PATH: tests/cypress/integration/help.cy.js - jobs: deploy: runs-on: ubuntu-latest @@ -30,6 +27,20 @@ jobs: with: persist-credentials: false + # Playwright baseURL must match the real WordPress home URL (include subdirectory installs). + # Trim, default to https, strip trailing slash so /wp-login.php resolves correctly. + - name: Normalize site URL for Playwright + env: + SITE_URL: ${{ vars.SITE_URL }} + run: | + set -euo pipefail + SITE="${SITE_URL//[[:space:]]/}" + if [[ -z "$SITE" ]]; then echo "vars.SITE_URL is empty"; exit 1; fi + if [[ "$SITE" != http://* && "$SITE" != https://* ]]; then SITE="https://$SITE"; fi + SITE="${SITE%/}" + echo "BASE_URL=$SITE" >> "$GITHUB_ENV" + echo "Using BASE_URL=$SITE" + - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 with: @@ -86,22 +97,21 @@ jobs: SERVER_IP: ${{ vars.SERVER_IP }} SERVER_PATH: ${{ vars.SERVER_PATH }} run: | - ssh -i github-actions -o StrictHostKeyChecking=no "$SERVER_USERNAME@$SERVER_IP" << 'EOF' + ssh -i github-actions -o StrictHostKeyChecking=no "$SERVER_USERNAME@$SERVER_IP" \ + "PLUGIN_NAME='$PLUGIN_NAME' SERVER_PATH='$SERVER_PATH' bash -s" << 'EOF' cd "${SERVER_PATH}/wp-content" - wp plugin install uploads/$PLUGIN_NAME.zip --force --path=./../ - rm uploads/$PLUGIN_NAME.zip - wp plugin activate $PLUGIN_NAME --path=./../ + wp plugin install "uploads/${PLUGIN_NAME}.zip" --force --path=./../ + rm "uploads/${PLUGIN_NAME}.zip" + wp plugin activate "${PLUGIN_NAME}" --path=./../ EOF - name: Cleanup SSH Key run: rm -f github-actions - name: Check if the remote Server is up - env: - SITE_URL: ${{ vars.SITE_URL }} run: | for _ in {1..30}; do - STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$SITE_URL/wp-login.php") + STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/wp-login.php") if [[ "$STATUS_CODE" == "200" ]]; then echo "Server is up with status code: $STATUS_CODE" exit 0 @@ -111,15 +121,39 @@ jobs: done echo "Server not ready after 5 minutes" && exit 1 - - name: Run Specific Cypress Tests - uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1 - with: - install: true - start: npm start - wait-on: ${{ vars.SITE_URL }} - config: baseUrl=${{ vars.SITE_URL }} - command: npx cypress run --spec ${{ env.CYPRESS_TEST_PATH }} + - name: Verify wp-login is WordPress (not 404 HTML) + run: | + set -euo pipefail + LOGIN_URL="${BASE_URL}/wp-login.php" + BODY_FILE="$(mktemp)" + CODE=$(curl -sS -o "$BODY_FILE" -w "%{http_code}" "$LOGIN_URL") + echo "HTTP $CODE for $LOGIN_URL" + if [[ "$CODE" != "200" ]]; then + head -c 800 "$BODY_FILE" || true + exit 1 + fi + if ! grep -qE 'id="user_login"|name="log"|wp-login' "$BODY_FILE"; then + echo "Response is not a WordPress login page (check SITE_URL matches WP home, including subdirectory)." + head -c 1200 "$BODY_FILE" || true + exit 1 + fi + rm -f "$BODY_FILE" + + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: Run Playwright Tests + run: npx playwright test --reporter=line env: - BASE_URL: ${{ vars.SITE_URL }} + CI: true + BASE_URL: ${{ env.BASE_URL }} WP_ADMIN_USERNAME: ${{ secrets.WP_ADMIN_USERNAME }} WP_ADMIN_PASSWORD: ${{ secrets.WP_ADMIN_PASSWORD }} + + - name: Store Playwright test results + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: playwright-report-${{ matrix.environment }} + path: tests/playwright/test-results/ + retention-days: 10 diff --git a/playwright.config.mjs b/playwright.config.mjs index 510a7fe04..e032d6ba8 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -60,8 +60,33 @@ process.env.WP_ADMIN_PASSWORD = process.env.WP_ADMIN_PASSWORD || 'password'; process.env.WP_VERSION = process.env.WP_VERSION || wpVersion; process.env.PHP_VERSION = process.env.PHP_VERSION || phpVersion; +/** + * Paths passed to page.goto('/foo') are resolved against the URL *origin* only, not baseURL's path. + * For WordPress in a subdirectory, baseURL must end with '/' and navigations must use relative paths + * (e.g. 'wp-login.php'), or /foo incorrectly hits https://domain/foo instead of https://domain/blog/foo. + * @param {string} raw + * @param {string} fallback e.g. http://localhost:8882 + */ +function normalizePlaywrightBaseURL(raw, fallback) { + const base = String(raw || fallback).trim(); + try { + const u = new URL(base); + if (u.pathname !== '/' && !u.pathname.endsWith('/')) { + u.pathname += '/'; + } + return u.href; + } catch { + return base; + } +} + +const resolvedBaseURL = normalizePlaywrightBaseURL( + process.env.BASE_URL, + `http://localhost:${_port}` +); + export default defineConfig({ - globalSetup: resolve(__dirname, './tests/playwright/global-setup.js'), + globalSetup: process.env.BASE_URL ? undefined : resolve(__dirname, './tests/playwright/global-setup.js'), projects: projects, testIgnore: [ // Don't ignore anything - we want to include gitignored files that playwright needs to find @@ -71,7 +96,7 @@ export default defineConfig({ ...devices['Desktop Chrome'], headless: true, viewport: { width: 1200, height: 800 }, - baseURL: `http://localhost:${_port}`, // Use port from wp-env.json + baseURL: resolvedBaseURL, ignoreHTTPSErrors: true, // WordPress-optimized settings locale: 'en-US', diff --git a/tests/playwright/global-setup.js b/tests/playwright/global-setup.js index 9cc0887fc..0dee184b8 100644 --- a/tests/playwright/global-setup.js +++ b/tests/playwright/global-setup.js @@ -31,18 +31,16 @@ async function globalSetup(config) { const permalinkStructure = '/%postname%/'; utils.fancyLog(`🔗 Setting permalink structure to: ${permalinkStructure}`, 100, 'gray', ''); - execSync(`npx wp-env run cli wp option update permalink_structure '${permalinkStructure}'`, { + wordpress.wpCli(`option update permalink_structure '${permalinkStructure}'`, { cwd: pluginRoot, - stdio: 'inherit', - encoding: 'utf-8', + failOnNonZeroExit: false, }); - + // Flush rewrite rules to apply the new permalink structure - utils.fancyLog('🔄 Flushing rewrite rules...', 100, 'gray', ''); - execSync('npx wp-env run cli wp rewrite flush', { + utils.fancyLog('🔄 Flushing rewrite rules with hard mode...', 100, 'gray', ''); + wordpress.wpCli('rewrite flush --hard', { cwd: pluginRoot, - stdio: 'inherit', - encoding: 'utf-8', + failOnNonZeroExit: false, }); // remove extra plugins for faster cleaner tests @@ -54,7 +52,7 @@ async function globalSetup(config) { 'wordpress-seo/wp-seo.php', ]; for (const plugin of extraPlugins) { - wordpress.wpCli(`plugin delete ${plugin}`, { + wordpress.wpCli(`plugin deactivate ${plugin} --uninstall`, { failOnNonZeroExit: false, }); } diff --git a/tests/playwright/helpers/auth.mjs b/tests/playwright/helpers/auth.mjs index fd91dba10..cd979395b 100644 --- a/tests/playwright/helpers/auth.mjs +++ b/tests/playwright/helpers/auth.mjs @@ -33,7 +33,7 @@ async function isLoggedIn(page) { } // If we're not on an admin page, try to access a protected page - const response = await page.goto('/wp-admin/', { waitUntil: 'domcontentloaded', timeout: 5000 }); + const response = await page.goto('wp-admin/', { waitUntil: 'domcontentloaded', timeout: 5000 }); // Check if we were redirected to login page const newUrl = page.url(); @@ -71,20 +71,30 @@ async function loginToWordPress(page, options = {}) { return; } - await page.goto('/wp-login.php'); + await page.goto('wp-login.php'); await page.fill('#user_login', username); await page.fill('#user_pass', password); await page.press('#user_pass', 'Enter'); - - // Wait for successful login: either left wp-login or landed on admin email verification (still wp-login.php?action=confirm_admin_email) - await page.waitForURL( - (url) => { - if (!url.pathname.includes('/wp-login.php')) return true; - if (url.searchParams.get('action') === 'confirm_admin_email') return true; - return false; - }, - { timeout: 10000 } - ); + + const loginSucceeded = (url) => { + if (!url.pathname.includes('/wp-login.php')) return true; + if (url.searchParams.get('action') === 'confirm_admin_email') return true; + return false; + }; + + try { + await page.waitForURL(loginSucceeded, { timeout: 20000 }); + } catch (err) { + const errorLocator = page.locator('#login_error, .login .message.error').first(); + if (await errorLocator.isVisible().catch(() => false)) { + const text = (await errorLocator.innerText()).trim(); + throw new Error( + `WordPress login failed (${text || 'invalid username or password'}). ` + + 'Set WP_ADMIN_USERNAME and WP_ADMIN_PASSWORD to real admin credentials for this site (GitHub secrets must not be placeholder text).' + ); + } + throw err; + } } /** @@ -131,7 +141,7 @@ async function navigateToAdminPage(page, adminPage, options = {}) { } // Navigate to the admin page - const response = await page.goto(`/wp-admin/${adminPage}`, { waitUntil: 'domcontentloaded' }); + const response = await page.goto(`wp-admin/${adminPage}`, { waitUntil: 'domcontentloaded' }); // Check if we were redirected to login (session expired) const currentUrl = page.url(); @@ -139,7 +149,7 @@ async function navigateToAdminPage(page, adminPage, options = {}) { // Session expired, login again await loginToWordPress(page, { ...options, force: true }); // Retry navigation - await page.goto(`/${adminPage}`, { waitUntil: 'domcontentloaded' }); + await page.goto(`wp-admin/${adminPage}`, { waitUntil: 'domcontentloaded' }); } // Create WordPress utilities for additional functionality diff --git a/tests/playwright/helpers/newfold.mjs b/tests/playwright/helpers/newfold.mjs index 9092f7197..7845fce73 100644 --- a/tests/playwright/helpers/newfold.mjs +++ b/tests/playwright/helpers/newfold.mjs @@ -324,7 +324,7 @@ async function logCapabilities() { * @returns {Promise} True if coming soon is enabled */ async function isComingSoonEnabled(page) { - const response = await page.request.get('/wp-json/wp/v2/options/nfd_coming_soon'); + const response = await page.request.get('wp-json/wp/v2/options/nfd_coming_soon'); if (response.ok()) { const data = await response.json(); return data === '1' || data === true; @@ -428,7 +428,7 @@ async function waitForDashboardWidgets(page, timeout = 10000) { * @param {string} path - The path within the plugin (e.g., '#/home'). */ async function navigateToPluginPage(page, pluginId, path = '') { - await page.goto(`/wp-admin/admin.php?page=${pluginId}${path}`); + await page.goto(`wp-admin/admin.php?page=${pluginId}${path}`); await waitForWordPressAdmin(page); } @@ -461,7 +461,7 @@ async function getAdminMenuItems(page) { */ async function waitForRestAPI(page) { // Try to access a simple REST endpoint - const response = await page.request.get('/wp-json/wp/v2/users/me'); + const response = await page.request.get('wp-json/wp/v2/users/me'); if (!response.ok()) { throw new Error('WordPress REST API not available'); } diff --git a/tests/playwright/helpers/wordpress.mjs b/tests/playwright/helpers/wordpress.mjs index f09192906..5bfe753f9 100644 --- a/tests/playwright/helpers/wordpress.mjs +++ b/tests/playwright/helpers/wordpress.mjs @@ -64,13 +64,20 @@ async function isPluginActive(page, pluginSlug) { * @param {string} command - WP-CLI command to execute * @returns {string|number} - Output string if available, 0 for success, or error info. */ -async function wpCli(command) { +async function wpCli(command, options = {}) { + // TODO + // bail early if no cli access (live site or not wp-env setup) + + const { timeout, failOnNonZeroExit = true } = options; + utils.fancyLog(`🔧 WP-CLI command: ${command}`); try { const output = execSync(`npx wp-env run cli wp ${command}`, { cwd: process.env.PLUGIN_DIR || process.cwd(), encoding: 'utf-8', // auto convert Buffer to string stdio: ['pipe', 'pipe', 'pipe'], // capture stdout/stderr + ...(timeout !== undefined ? { timeout } : {}), + ...(failOnNonZeroExit !== undefined ? { failOnNonZeroExit } : {}), }); // If output is empty, just return 0 for success @@ -78,6 +85,10 @@ async function wpCli(command) { } catch (err) { // err.status = exit code // err.stdout / err.stderr may have useful info + if (failOnNonZeroExit) { + const detail = err.stderr ? err.stderr.toString().trim() : err.message; + throw new Error(`wp ${command}: ${detail}`); + } if (err.stderr) { return `Error: ${err.stderr.toString().trim()}`; } diff --git a/tests/playwright/specs/dashboard-widgets.spec.js b/tests/playwright/specs/dashboard-widgets.spec.js index 7d333b86b..7de6e9292 100644 --- a/tests/playwright/specs/dashboard-widgets.spec.js +++ b/tests/playwright/specs/dashboard-widgets.spec.js @@ -10,9 +10,9 @@ test.describe('Dashboard Widgets', () => { await newfold.clearCapabilities(); }); - test('Bluehost Widgets are all Accessible', async ({ page }) => { + test('Bluehost Widgets are all Accessible', { tag: '@smoke' }, async ({ page }) => { // Wait for dashboard widgets to load with longer timeout - await expect(page).toHaveURL('http://localhost:8882/wp-admin/index.php'); + await expect(page).toHaveURL(/wp-admin\/index\.php$/); try { await newfold.waitForDashboardWidgets(page, 15000); @@ -138,7 +138,7 @@ test.describe('Dashboard Widgets', () => { await expect(enableComingSoonButton).toHaveAttribute('href', '#'); }); - test('Help Widget', async ({ page }) => { + test('Help Widget', { tag: '@smoke' }, async ({ page }) => { const helpWidget = page.locator('#bluehost_help_widget'); await expect(helpWidget).toBeVisible(); @@ -172,7 +172,7 @@ test.describe('Dashboard Widgets', () => { await expect(helpCenter).toBeVisible(); }); - test('Bluehost Account Widget', async ({ page }) => { + test('Bluehost Account Widget', { tag: '@smoke' },async ({ page }) => { const accountWidget = page.locator('#bluehost_account_widget'); await expect(accountWidget).toBeVisible(); diff --git a/tests/playwright/specs/vrt.spec.js b/tests/playwright/specs/vrt.spec.js index 2194badbd..b6e3420ef 100644 --- a/tests/playwright/specs/vrt.spec.js +++ b/tests/playwright/specs/vrt.spec.js @@ -22,8 +22,8 @@ const paths = [ 'wp-admin/admin.php?page=' + pluginId + '#/commerce', 'wp-admin/admin.php?page=' + pluginId + '#/marketplace', 'wp-admin/admin.php?page=' + pluginId + '#/help', - '/wp-admin/plugins.php', - '/wp-admin/plugin-install.php', + 'wp-admin/plugins.php', + 'wp-admin/plugin-install.php', 'wp-admin/plugin-install.php?tab=premium-marketplace' ];