diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e35e1518..34bae337 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: jobs: lint: runs-on: ubuntu-latest + timeout-minutes: 8 steps: - uses: actions/checkout@v4 @@ -29,6 +30,7 @@ jobs: test: runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 8 needs: lint strategy: @@ -60,6 +62,7 @@ jobs: test-site: runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 8 needs: lint steps: @@ -109,6 +112,7 @@ jobs: test-reporter: runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 8 needs: lint steps: @@ -151,6 +155,7 @@ jobs: test-tui: runs-on: ubuntu-latest + timeout-minutes: 8 needs: lint steps: @@ -196,6 +201,7 @@ jobs: test-ruby-client: runs-on: ubuntu-latest + timeout-minutes: 8 needs: [lint, changes-ruby] if: needs.changes-ruby.outputs.ruby == 'true' @@ -303,8 +309,24 @@ jobs: - '.github/workflows/ci.yml' - '.github/workflows/release-swift-client.yml' + changes-ember: + runs-on: ubuntu-latest + outputs: + ember: ${{ steps.filter.outputs.ember }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + ember: + - 'clients/ember/**' + - '.github/workflows/ci.yml' + - '.github/workflows/release-ember-client.yml' + test-storybook-client: runs-on: ubuntu-latest + timeout-minutes: 8 needs: [lint, changes-storybook] if: needs.changes-storybook.outputs.storybook == 'true' @@ -346,6 +368,7 @@ jobs: test-static-site-client: runs-on: ubuntu-latest + timeout-minutes: 8 needs: [lint, changes-static-site] if: needs.changes-static-site.outputs.static-site == 'true' @@ -387,6 +410,7 @@ jobs: test-vitest-client: runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 8 needs: [lint, changes-vitest] if: needs.changes-vitest.outputs.vitest == 'true' @@ -440,6 +464,7 @@ jobs: CI: true - name: Run Vitest client E2E tests + if: matrix.node-version == 22 working-directory: ./clients/vitest run: ../../bin/vizzly.js run "npm run test:e2e" env: @@ -450,6 +475,7 @@ jobs: test-swift-client: runs-on: macos-latest + timeout-minutes: 8 needs: [lint, changes-swift] if: needs.changes-swift.outputs.swift == 'true' @@ -471,9 +497,84 @@ jobs: working-directory: ./clients/swift run: swift test + test-ember-client: + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 8 + needs: [lint, changes-ember] + if: needs.changes-ember.outputs.ember == 'true' + + strategy: + matrix: + node-version: [22, 24] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install CLI dependencies + run: npm ci + + - name: Build CLI + run: npm run build + + - name: Install Ember client dependencies + working-directory: ./clients/ember + run: npm install + + - name: Get installed Playwright version + working-directory: ./clients/ember + id: playwright-version + run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-chromium + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: ./clients/ember + run: npx playwright install chromium --with-deps + + - name: Run linter + working-directory: ./clients/ember + run: npm run lint + + - name: Run unit tests + working-directory: ./clients/ember + run: npm test + env: + CI: true + + - name: Run integration tests + working-directory: ./clients/ember + run: npm run test:integration + env: + CI: true + + - name: Run Ember E2E visual tests + if: matrix.node-version == 22 + working-directory: ./clients/ember + run: | + cd test-app + npm install + npm run build -- --mode development + ../../../bin/vizzly.js run "npx testem ci --file testem.cjs" + env: + CI: true + VIZZLY_TOKEN: ${{ secrets.VIZZLY_EMBER_CLIENT_TOKEN }} + VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }} + VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }} + ci-check: runs-on: ubuntu-latest - needs: [lint, test, test-site, test-reporter, test-tui, changes-ruby, test-ruby-client, changes-storybook, test-storybook-client, changes-static-site, test-static-site-client, changes-vitest, test-vitest-client, changes-swift, test-swift-client] + needs: [lint, test, test-site, test-reporter, test-tui, changes-ruby, test-ruby-client, changes-storybook, test-storybook-client, changes-static-site, test-static-site-client, changes-vitest, test-vitest-client, changes-swift, test-swift-client, changes-ember, test-ember-client] if: always() steps: - name: Check if all jobs passed @@ -510,4 +611,9 @@ jobs: exit 1 fi + if [[ "${{ needs.changes-ember.outputs.ember }}" == "true" && "${{ needs.test-ember-client.result }}" == "failure" ]]; then + echo "Ember client tests failed" + exit 1 + fi + echo "All jobs passed" diff --git a/.github/workflows/release-ember-client.yml b/.github/workflows/release-ember-client.yml new file mode 100644 index 00000000..ae17bf0a --- /dev/null +++ b/.github/workflows/release-ember-client.yml @@ -0,0 +1,197 @@ +name: Release Ember Client + +on: + workflow_dispatch: + inputs: + version_type: + description: 'Version bump type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + prerelease: + description: 'Create prerelease (beta tag)?' + required: false + default: false + type: boolean + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Configure git + run: | + git config --local user.email "${{ secrets.GIT_USER_EMAIL }}" + git config --local user.name "${{ secrets.GIT_USER_NAME }}" + + - name: Ensure we have latest main + run: | + git fetch origin main + git reset --hard origin/main + + - name: Get current version + id: current_version + working-directory: ./clients/ember + run: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + - name: Bump version + id: version + working-directory: ./clients/ember + run: | + if [ "${{ github.event.inputs.prerelease }}" == "true" ]; then + NEW_VERSION=$(npm version pre${{ github.event.inputs.version_type }} --preid=beta --no-git-tag-version | sed 's/v//') + else + NEW_VERSION=$(npm version ${{ github.event.inputs.version_type }} --no-git-tag-version | sed 's/v//') + fi + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "tag=ember/v$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Generate changelog with Claude + id: changelog + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + Generate release notes for the Vizzly Ember SDK v${{ steps.version.outputs.version }}. + + Context: + - Client location: `clients/ember/` + - Previous tag: `ember/v${{ steps.current_version.outputs.version }}` + - New version: `${{ steps.version.outputs.version }}` + - This is a monorepo with multiple clients and a CLI + + Instructions: + 1. Use git commands (via Bash tool) to get commits since last ember/* tag + 2. Analyze which commits are relevant to `clients/ember/` + 3. Read the code changes if needed to understand impact + 4. Generate user-friendly release notes with categories: Added, Changed, Fixed + 5. Focus on user-facing changes only + 6. If no relevant changes, output: "No changes to Ember SDK in this release" + + Save the changelog to `clients/ember/CHANGELOG-RELEASE.md` with this format: + + ## What's Changed + + ### Added + - New features + + ### Changed + - Breaking or notable changes + + ### Fixed + - Bug fixes + + **Full Changelog**: https://github.com/vizzly-testing/cli/compare/ember/v${{ steps.current_version.outputs.version }}...ember/v${{ steps.version.outputs.version }} + claude_args: '--allowed-tools "Bash(git:*),Write"' + + - name: Install dependencies + working-directory: ./clients/ember + run: npm install + + - name: Run tests + working-directory: ./clients/ember + run: npm test + + - name: Run linter + working-directory: ./clients/ember + run: npm run lint + + - name: Update CHANGELOG.md + working-directory: ./clients/ember + run: | + # Check if changelog was generated successfully + if [ ! -f CHANGELOG-RELEASE.md ]; then + echo "Warning: CHANGELOG-RELEASE.md not found, creating fallback changelog" + cat > CHANGELOG-RELEASE.md << 'EOF' + ## What's Changed + + Release v${{ steps.version.outputs.version }} + + See the full diff for detailed changes. + EOF + fi + + # Prepend new release to CHANGELOG.md + echo -e "# Changelog\n" > CHANGELOG-NEW.md + echo "All notable changes to this project will be documented in this file." >> CHANGELOG-NEW.md + echo "" >> CHANGELOG-NEW.md + echo "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)," >> CHANGELOG-NEW.md + echo "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)." >> CHANGELOG-NEW.md + echo "" >> CHANGELOG-NEW.md + echo "## [${{ steps.version.outputs.version }}] - $(date +%Y-%m-%d)" >> CHANGELOG-NEW.md + echo "" >> CHANGELOG-NEW.md + cat CHANGELOG-RELEASE.md >> CHANGELOG-NEW.md + echo "" >> CHANGELOG-NEW.md + tail -n +8 CHANGELOG.md >> CHANGELOG-NEW.md + mv CHANGELOG-NEW.md CHANGELOG.md + rm CHANGELOG-RELEASE.md + + - name: Reconfigure git auth + run: | + git config --local user.email "${{ secrets.GIT_USER_EMAIL }}" + git config --local user.name "${{ secrets.GIT_USER_NAME }}" + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} | base64)" + + - name: Commit and push changes + run: | + git add clients/ember/package.json clients/ember/CHANGELOG.md + git commit -m "🔖 Ember SDK v${{ steps.version.outputs.version }}" + git push origin main + git tag "${{ steps.version.outputs.tag }}" + git push origin "${{ steps.version.outputs.tag }}" + + - name: Publish to npm + working-directory: ./clients/ember + run: | + if [ "${{ github.event.inputs.prerelease }}" == "true" ]; then + npm publish --provenance --access public --tag beta + else + npm publish --provenance --access public + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Read changelog for release + id: release_notes + working-directory: ./clients/ember + run: | + # Extract just this version's changelog + CHANGELOG=$(sed -n '/## \[${{ steps.version.outputs.version }}\]/,/## \[/p' CHANGELOG.md | sed '$ d') + { + echo 'notes<> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: 🐹 Ember SDK v${{ steps.version.outputs.version }} + body: ${{ steps.release_notes.outputs.notes }} + files: ./clients/ember/*.tgz + draft: false + prerelease: ${{ github.event.inputs.prerelease }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/clients/ember/CHANGELOG.md b/clients/ember/CHANGELOG.md new file mode 100644 index 00000000..04b441b8 --- /dev/null +++ b/clients/ember/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.1-beta.0] - 2026-01-04 + +## What's Changed + +### Added + +- Initial beta release of Vizzly Ember SDK for visual testing with Testem +- Custom Testem launcher using Playwright for browser control (Chromium, Firefox, WebKit) +- `vizzlySnapshot()` helper for capturing screenshots in acceptance tests +- Automatic viewport sizing and `#ember-testing` container expansion +- TDD server auto-discovery via `.vizzly/server.json` +- Mobile viewport testing with customizable dimensions +- Support for both TDD mode (local comparison) and cloud mode + +### Architecture + +The SDK uses a snapshot server pattern: +- Browser tests call `vizzlySnapshot()` which sends requests to a local snapshot server +- Snapshot server uses Playwright to capture screenshots +- Screenshots are forwarded to the Vizzly TDD server for comparison + +**Full Changelog**: https://github.com/vizzly-testing/cli/commits/ember/v0.0.1-beta.0 + +[0.0.1-beta.0]: https://github.com/vizzly-testing/cli/releases/tag/ember/v0.0.1-beta.0 diff --git a/clients/ember/LICENSE b/clients/ember/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/clients/ember/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/clients/ember/bin/vizzly-browser.js b/clients/ember/bin/vizzly-browser.js index 7b683834..5576eef0 100755 --- a/clients/ember/bin/vizzly-browser.js +++ b/clients/ember/bin/vizzly-browser.js @@ -46,16 +46,16 @@ async function cleanup() { if (browserInstance) { await closeBrowser(browserInstance); } - } catch (error) { - console.error('[vizzly-browser] Error closing browser:', error.message); + } catch { + // Ignore cleanup errors } try { if (snapshotServer) { await stopSnapshotServer(snapshotServer); } - } catch (error) { - console.error('[vizzly-browser] Error stopping server:', error.message); + } catch { + // Ignore cleanup errors } process.exit(0); @@ -79,11 +79,58 @@ async function main() { // Set page reference immediately when page is created // This happens BEFORE navigation so tests can capture screenshots setPage(page); + + // Listen for page close - this means browser was closed + page.on('close', cleanup); }, }); - // 3. Keep process alive - Testem will send SIGTERM when done - // The browser will run tests and the snapshot server will handle requests + // 3. Monitor for test completion + // Hook into the test framework (QUnit or Mocha) to detect when tests finish + let { page } = browserInstance; + + // Wait for a test framework to be available, then hook into its completion + await page.evaluate(() => { + return new Promise(resolve => { + let checkFramework = () => { + // Check for QUnit + if (typeof QUnit !== 'undefined') { + QUnit.done(() => { + console.log('[testem-vizzly] all-tests-complete'); + }); + resolve(); + return; + } + + // Check for Mocha + if (typeof Mocha !== 'undefined' || typeof mocha !== 'undefined') { + let Runner = (typeof Mocha !== 'undefined' ? Mocha : mocha).Runner; + let originalEmit = Runner.prototype.emit; + Runner.prototype.emit = function (evt) { + if (evt === 'end') { + console.log('[testem-vizzly] all-tests-complete'); + } + return originalEmit.apply(this, arguments); + }; + resolve(); + return; + } + + // Keep checking until a framework is found + requestAnimationFrame(checkFramework); + }; + checkFramework(); + }); + }); + + // Listen for the completion signal + page.on('console', msg => { + if (msg.text() === '[testem-vizzly] all-tests-complete') { + cleanup(); + } + }); + + // 4. Keep process alive until cleanup is called await new Promise(() => {}); } catch (error) { console.error('[vizzly-browser] Failed to start:', error.message); diff --git a/clients/ember/package.json b/clients/ember/package.json index fdfc7308..c363fd62 100644 --- a/clients/ember/package.json +++ b/clients/ember/package.json @@ -1,6 +1,6 @@ { "name": "@vizzly-testing/ember", - "version": "0.1.0", + "version": "0.0.1-beta.0", "description": "Visual testing SDK for Ember.js projects using Testem", "keywords": [ "vizzly", diff --git a/clients/ember/src/launcher/browser.js b/clients/ember/src/launcher/browser.js index abf9cc6a..bd4aded4 100644 --- a/clients/ember/src/launcher/browser.js +++ b/clients/ember/src/launcher/browser.js @@ -105,9 +105,9 @@ export async function launchBrowser(browserType, testUrl, options = {}) { onPageCreated(page); } - // Navigate to test URL and wait for network to be idle + // Navigate to test URL and wait for load (not networkidle - Socket.IO keeps network active) await page.goto(testUrl, { - waitUntil: 'networkidle', + waitUntil: 'load', timeout: 60000, }); diff --git a/clients/ember/src/launcher/snapshot-server.js b/clients/ember/src/launcher/snapshot-server.js index 63526efd..797b3b98 100644 --- a/clients/ember/src/launcher/snapshot-server.js +++ b/clients/ember/src/launcher/snapshot-server.js @@ -8,7 +8,8 @@ */ import { existsSync, readFileSync } from 'node:fs'; -import { createServer } from 'node:http'; +import { createServer, request as httpRequest } from 'node:http'; +import { request as httpsRequest } from 'node:https'; import { dirname, join, parse } from 'node:path'; /** @@ -93,18 +94,64 @@ async function forwardToVizzly(name, imageBuffer, properties = {}) { }, }; - let response = await fetch(`${tddServerUrl}/screenshot`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); + // Use node:http directly with Connection: close to prevent keep-alive hangs + let result = await httpPost(`${tddServerUrl}/screenshot`, payload); + return result; +} - if (!response.ok) { - let errorBody = await response.text(); - throw new Error(`Vizzly server error: ${response.status} - ${errorBody}`); - } +/** + * Make HTTP POST request without keep-alive (prevents process hang on shutdown) + * @param {string} url - Target URL + * @param {Object} data - JSON payload + * @returns {Promise} Parsed JSON response + */ +function httpPost(url, data) { + return new Promise((resolve, reject) => { + let parsedUrl = new URL(url); + let isHttps = parsedUrl.protocol === 'https:'; + let requestFn = isHttps ? httpsRequest : httpRequest; + + let body = JSON.stringify(data); + + let options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + Connection: 'close', // Disable keep-alive + }, + agent: false, // Don't use connection pooling + }; + + let req = requestFn(options, res => { + let chunks = []; + + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => { + let responseBody = Buffer.concat(chunks).toString(); + + if (res.statusCode >= 400) { + reject( + new Error(`Vizzly server error: ${res.statusCode} - ${responseBody}`) + ); + return; + } + + try { + resolve(JSON.parse(responseBody)); + } catch { + resolve({ raw: responseBody }); + } + }); + }); - return await response.json(); + req.on('error', reject); + req.write(body); + req.end(); + }); } /** @@ -185,6 +232,7 @@ export async function startSnapshotServer() { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Connection', 'close'); // Prevent keep-alive hangs // Handle preflight if (req.method === 'OPTIONS') { @@ -226,6 +274,10 @@ export async function startSnapshotServer() { export async function stopSnapshotServer(serverInfo) { return new Promise(resolve => { if (serverInfo?.server) { + // Force close all keep-alive connections (Node 18.2+) + if (serverInfo.server.closeAllConnections) { + serverInfo.server.closeAllConnections(); + } serverInfo.server.close(() => resolve()); } else { resolve(); diff --git a/clients/ember/tests/integration/e2e.test.js b/clients/ember/tests/integration/e2e.test.js index 49e55365..3cd91208 100644 --- a/clients/ember/tests/integration/e2e.test.js +++ b/clients/ember/tests/integration/e2e.test.js @@ -12,7 +12,7 @@ import assert from 'node:assert'; import { spawn } from 'node:child_process'; -import { existsSync, mkdirSync, rmSync } from 'node:fs'; +import { mkdirSync, rmSync } from 'node:fs'; import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -64,7 +64,7 @@ describe('e2e with TDD server', { skip: !process.env.RUN_E2E }, () => { }); // Start test page server - testServer = createServer((req, res) => { + testServer = createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` @@ -146,7 +146,7 @@ describe('e2e without TDD server', () => { before(async () => { // Start test page server - testServer = createServer((req, res) => { + testServer = createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` diff --git a/clients/ember/tests/integration/launcher.test.js b/clients/ember/tests/integration/launcher.test.js index bf683290..1da411e8 100644 --- a/clients/ember/tests/integration/launcher.test.js +++ b/clients/ember/tests/integration/launcher.test.js @@ -26,7 +26,7 @@ describe('launcher integration', () => { before(async () => { // Start a simple test page server - testServer = createServer((req, res) => { + testServer = createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(`