Skip to content

Commit ef8a406

Browse files
authored
✨ Add Ember.js SDK for visual testing with Testem (#166)
## Summary Introduces `@vizzly-testing/ember` package that enables visual regression testing in Ember.js projects using Testem and Playwright. - Custom Testem launcher that uses Playwright for browser control (supports Chromium, Firefox, WebKit) - `vizzlySnapshot()` helper for capturing screenshots in acceptance tests - Automatic viewport sizing and `#ember-testing` container expansion (captures just the app, not QUnit UI) - TDD server auto-discovery via `.vizzly/server.json` - Mobile viewport testing with customizable dimensions - Test app fixture for SDK development and verification ### Architecture ``` Browser (Ember test) │ │ fetch() to snapshot server ▼ Snapshot Server (Node.js) │ │ Playwright screenshot │ │ forward to TDD server ▼ Vizzly TDD Server │ │ Compare, save baselines ▼ .vizzly/baselines/ ``` ### Usage ```javascript // testem.cjs const { configure } = require('@vizzly-testing/ember'); module.exports = configure({ cwd: 'dist', test_page: 'tests/index.html?hidepassed', launch_in_ci: ['Chrome'], }); ``` ```javascript // tests/acceptance/visual-test.js import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; test('captures homepage', async function(assert) { await visit('/'); await vizzlySnapshot('homepage'); await vizzlySnapshot('homepage-mobile', { width: 375, height: 667 }); }); ``` ## Test plan - [x] Unit tests for testem-config and snapshot-server - [x] Integration tests for launcher - [x] E2E test with real Ember app (`npm run test:ember` in clients/ember) - [x] Verified screenshots capture just the app content at correct viewport sizes
1 parent 61b1b3c commit ef8a406

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+30384
-0
lines changed

clients/ember/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Test app build artifacts (keep source, ignore build output)
5+
test-app/node_modules/
6+
test-app/dist/
7+
test-app/.vizzly/
8+
test-app/tmp/

clients/ember/README.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# @vizzly-testing/ember
2+
3+
Visual testing SDK for Ember.js projects using Testem. Capture screenshots from your acceptance and integration tests and compare them with Vizzly.
4+
5+
## Installation
6+
7+
```bash
8+
npm install -D @vizzly-testing/ember
9+
10+
# Install browser (Chromium is recommended)
11+
npx playwright install chromium
12+
```
13+
14+
## Setup
15+
16+
### 1. Configure Testem
17+
18+
Wrap your `testem.js` configuration with the `configure()` function:
19+
20+
```javascript
21+
// testem.js (or testem.cjs for ES modules projects)
22+
const { configure } = require('@vizzly-testing/ember');
23+
24+
module.exports = configure({
25+
// For Ember with Vite/Embroider, Testem must serve from dist/
26+
cwd: 'dist',
27+
test_page: 'tests/index.html?hidepassed',
28+
disable_watching: true,
29+
launch_in_ci: ['Chrome'],
30+
launch_in_dev: ['Chrome']
31+
});
32+
```
33+
34+
The `configure()` function replaces standard browser launchers (Chrome, Firefox, Safari) with Playwright-powered launchers that can capture screenshots.
35+
36+
> **Note for Ember + Vite projects**: The `cwd: 'dist'` option is required because Vite builds test files into the `dist/` directory. Without this, Testem won't find your test assets.
37+
38+
### 2. Write Tests with Snapshots
39+
40+
Import `vizzlySnapshot` in your test files:
41+
42+
```javascript
43+
// tests/acceptance/dashboard-test.js
44+
import { module, test } from 'qunit';
45+
import { visit } from '@ember/test-helpers';
46+
import { setupApplicationTest } from 'ember-qunit';
47+
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
48+
49+
module('Acceptance | Dashboard', function(hooks) {
50+
setupApplicationTest(hooks);
51+
52+
test('renders empty state', async function(assert) {
53+
await visit('/dashboard');
54+
55+
// Capture screenshot
56+
await vizzlySnapshot('dashboard-empty');
57+
58+
assert.dom('[data-test-empty-state]').exists();
59+
});
60+
61+
test('renders with data', async function(assert) {
62+
// ... setup data
63+
await visit('/dashboard');
64+
65+
// Capture specific element
66+
await vizzlySnapshot('dashboard-table', {
67+
selector: '[data-test-data-table]'
68+
});
69+
70+
assert.dom('[data-test-data-table]').exists();
71+
});
72+
});
73+
```
74+
75+
### 3. Run Tests with Vizzly
76+
77+
```bash
78+
# Start the Vizzly TDD server
79+
vizzly tdd start
80+
81+
# Build with development mode (includes test files)
82+
npm run build -- --mode development
83+
84+
# Run your tests via Testem
85+
npx testem ci --file testem.cjs
86+
87+
# Or use ember test (which handles the build)
88+
ember test
89+
```
90+
91+
Screenshots are captured and compared locally. View results in the Vizzly dashboard at http://localhost:47392.
92+
93+
## API
94+
95+
### `configure(testemConfig)`
96+
97+
Wraps your Testem configuration to use Vizzly-powered browser launchers.
98+
99+
```javascript
100+
const { configure } = require('@vizzly-testing/ember');
101+
102+
module.exports = configure({
103+
// Your existing testem.js options
104+
launch_in_ci: ['Chrome'],
105+
launch_in_dev: ['Chrome'],
106+
});
107+
```
108+
109+
**Browser Mapping:**
110+
- `Chrome` → Uses Playwright Chromium
111+
- `Firefox` → Uses Playwright Firefox
112+
- `Safari` / `WebKit` → Uses Playwright WebKit
113+
114+
### `vizzlySnapshot(name, options?)`
115+
116+
Captures a screenshot and sends it to Vizzly for comparison. By default, captures just the `#ember-testing` container (your app), not the QUnit test runner UI.
117+
118+
```javascript
119+
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
120+
121+
// Basic usage - captures app at 1280x720
122+
await vizzlySnapshot('homepage');
123+
124+
// Mobile viewport
125+
await vizzlySnapshot('homepage-mobile', {
126+
width: 375,
127+
height: 667
128+
});
129+
130+
// Capture specific element within the app
131+
await vizzlySnapshot('login-form', {
132+
selector: '[data-test-login-form]'
133+
});
134+
135+
// Full options
136+
await vizzlySnapshot('screenshot-name', {
137+
// Viewport dimensions (default: 1280x720)
138+
width: 1280,
139+
height: 720,
140+
141+
// Capture specific element within #ember-testing
142+
selector: '[data-test-component]',
143+
144+
// What to capture: 'app' (default), 'container', or 'page'
145+
scope: 'app',
146+
147+
// Capture full scrollable content
148+
fullPage: false,
149+
150+
// Add custom metadata
151+
properties: {
152+
theme: 'dark',
153+
user: 'admin'
154+
}
155+
});
156+
```
157+
158+
**Options:**
159+
160+
| Option | Type | Default | Description |
161+
|--------|------|---------|-------------|
162+
| `width` | number | 1280 | Viewport width for the screenshot |
163+
| `height` | number | 720 | Viewport height for the screenshot |
164+
| `selector` | string | null | CSS selector to capture specific element |
165+
| `scope` | string | 'app' | What to capture: `'app'` (just #ember-testing), `'container'`, or `'page'` (full page including QUnit) |
166+
| `fullPage` | boolean | false | Capture full scrollable content |
167+
| `properties` | object | {} | Custom metadata attached to the snapshot |
168+
169+
The function automatically:
170+
- Waits for Ember's `settled()` before capturing
171+
- Expands the `#ember-testing` container to the specified viewport size
172+
- Captures just your app content (not the QUnit UI)
173+
174+
### `isVizzlyAvailable()`
175+
176+
Check if Vizzly is available in the current test environment.
177+
178+
```javascript
179+
import { isVizzlyAvailable } from '@vizzly-testing/ember/test-support';
180+
181+
if (isVizzlyAvailable()) {
182+
await vizzlySnapshot('conditional-snapshot');
183+
}
184+
```
185+
186+
## Browser Support
187+
188+
| Browser | Status |
189+
|---------|--------|
190+
| Chromium (Chrome, Edge) | Fully supported |
191+
| Firefox | Supported |
192+
| WebKit (Safari) | Supported |
193+
194+
Install browsers with:
195+
196+
```bash
197+
npx playwright install chromium
198+
npx playwright install firefox
199+
npx playwright install webkit
200+
```
201+
202+
## How It Works
203+
204+
1. **Testem Configuration**: The `configure()` wrapper replaces standard browser launchers with custom Vizzly launchers
205+
2. **Custom Launcher**: When Testem starts, it spawns `vizzly-browser` instead of the regular browser
206+
3. **Playwright Integration**: The launcher uses Playwright to control the browser and capture screenshots
207+
4. **Snapshot Server**: A local HTTP server receives screenshot requests from test code
208+
5. **Vizzly Integration**: Screenshots are forwarded to the Vizzly TDD server for comparison
209+
210+
## CI/CD
211+
212+
For CI environments, ensure:
213+
214+
1. Browsers are installed: `npx playwright install chromium`
215+
2. Vizzly token is set: `VIZZLY_TOKEN=your-token`
216+
217+
```yaml
218+
# GitHub Actions example
219+
- name: Install Playwright
220+
run: npx playwright install chromium
221+
222+
- name: Run Tests
223+
env:
224+
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
225+
run: ember test
226+
```
227+
228+
## Troubleshooting
229+
230+
### "No snapshot server available"
231+
232+
Tests must be run through Testem with the Vizzly-configured launchers. Ensure:
233+
- `testem.js` uses `configure()` wrapper
234+
- Running via `ember test` (not direct browser)
235+
236+
### "No Vizzly server found"
237+
238+
Start the TDD server before running tests:
239+
240+
```bash
241+
vizzly tdd start
242+
```
243+
244+
### Browser fails to launch
245+
246+
Install the required browser:
247+
248+
```bash
249+
npx playwright install chromium
250+
```
251+
252+
For CI environments, you may need additional dependencies:
253+
254+
```bash
255+
npx playwright install-deps chromium
256+
```
257+
258+
## License
259+
260+
MIT
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Vizzly Browser Launcher
4+
*
5+
* Custom browser launcher spawned by Testem. Uses Playwright to launch
6+
* a browser with screenshot capture capabilities.
7+
*
8+
* Usage: vizzly-browser <browser> <url>
9+
* browser: chromium | firefox | webkit
10+
* url: The test page URL (provided by Testem)
11+
*
12+
* @example
13+
* # Testem spawns this command:
14+
* npx vizzly-browser chromium http://localhost:7357/tests/index.html
15+
*/
16+
17+
import { closeBrowser, launchBrowser } from '../src/launcher/browser.js';
18+
import {
19+
setPage,
20+
startSnapshotServer,
21+
stopSnapshotServer,
22+
} from '../src/launcher/snapshot-server.js';
23+
24+
let [, , browserType, testUrl] = process.argv;
25+
26+
// Validate arguments
27+
if (!browserType || !testUrl) {
28+
console.error('Usage: vizzly-browser <browser> <url>');
29+
console.error(' browser: chromium | firefox | webkit');
30+
console.error(' url: Test page URL (provided by Testem)');
31+
process.exit(1);
32+
}
33+
34+
let browserInstance = null;
35+
let snapshotServer = null;
36+
let isShuttingDown = false;
37+
38+
/**
39+
* Clean up resources and exit
40+
*/
41+
async function cleanup() {
42+
if (isShuttingDown) return;
43+
isShuttingDown = true;
44+
45+
try {
46+
if (browserInstance) {
47+
await closeBrowser(browserInstance);
48+
}
49+
} catch (error) {
50+
console.error('[vizzly-browser] Error closing browser:', error.message);
51+
}
52+
53+
try {
54+
if (snapshotServer) {
55+
await stopSnapshotServer(snapshotServer);
56+
}
57+
} catch (error) {
58+
console.error('[vizzly-browser] Error stopping server:', error.message);
59+
}
60+
61+
process.exit(0);
62+
}
63+
64+
/**
65+
* Main launcher function
66+
*/
67+
async function main() {
68+
try {
69+
// 1. Start snapshot server first
70+
snapshotServer = await startSnapshotServer();
71+
let snapshotUrl = `http://127.0.0.1:${snapshotServer.port}`;
72+
73+
// 2. Launch browser with Playwright
74+
// Note: We set the page reference in launchBrowser before navigation
75+
// to avoid a race condition where tests run before page is set
76+
browserInstance = await launchBrowser(browserType, testUrl, {
77+
snapshotUrl,
78+
onPageCreated: page => {
79+
// Set page reference immediately when page is created
80+
// This happens BEFORE navigation so tests can capture screenshots
81+
setPage(page);
82+
},
83+
});
84+
85+
// 3. Keep process alive - Testem will send SIGTERM when done
86+
// The browser will run tests and the snapshot server will handle requests
87+
await new Promise(() => {});
88+
} catch (error) {
89+
console.error('[vizzly-browser] Failed to start:', error.message);
90+
91+
// Attempt cleanup before exiting
92+
if (snapshotServer) {
93+
await stopSnapshotServer(snapshotServer).catch(() => {});
94+
}
95+
96+
process.exit(1);
97+
}
98+
}
99+
100+
// Handle graceful shutdown signals from Testem
101+
process.on('SIGTERM', cleanup);
102+
process.on('SIGINT', cleanup);
103+
process.on('SIGHUP', cleanup);
104+
105+
// Handle unexpected errors
106+
process.on('uncaughtException', error => {
107+
console.error('[vizzly-browser] Uncaught exception:', error.message);
108+
cleanup();
109+
});
110+
111+
process.on('unhandledRejection', reason => {
112+
console.error('[vizzly-browser] Unhandled rejection:', reason);
113+
cleanup();
114+
});
115+
116+
main();

0 commit comments

Comments
 (0)