sunpeak 0.19.1 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +6 -3
  2. package/bin/commands/new.mjs +3 -1
  3. package/bin/commands/test.mjs +107 -46
  4. package/bin/lib/inspect/inspect-config.d.mts +4 -0
  5. package/bin/lib/inspect/inspect-config.mjs +2 -0
  6. package/bin/lib/test/base-config.mjs +16 -1
  7. package/bin/lib/test/test-config.d.mts +22 -0
  8. package/bin/lib/test/test-config.mjs +2 -0
  9. package/bin/lib/test/test-fixtures.d.mts +34 -1
  10. package/bin/lib/test/test-fixtures.mjs +43 -0
  11. package/package.json +1 -1
  12. package/template/README.md +6 -3
  13. package/template/dist/albums/albums.json +1 -1
  14. package/template/dist/carousel/carousel.json +1 -1
  15. package/template/dist/map/map.json +1 -1
  16. package/template/dist/review/review.json +1 -1
  17. package/template/package.json +5 -3
  18. package/template/test-results/.last-run.json +4 -0
  19. package/template/tests/e2e/visual.spec.ts +36 -0
  20. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-linux.png +0 -0
  21. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-linux.png +0 -0
  22. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-linux.png +0 -0
  23. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-linux.png +0 -0
  24. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-linux.png +0 -0
  25. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-linux.png +0 -0
  26. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-chatgpt-linux.png +0 -0
  27. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-claude-linux.png +0 -0
package/README.md CHANGED
@@ -107,10 +107,13 @@ sunpeak new
107
107
  | Command | Description |
108
108
  | ------------------------------------- | ------------------------------------------- |
109
109
  | `sunpeak inspect --server <url\|cmd>` | Inspect any MCP server in the inspector |
110
- | `sunpeak test` | Run e2e tests against the inspector |
111
- | `sunpeak test init` | Scaffold test infrastructure into a project |
112
- | `sunpeak test --unit` | Run unit tests (Vitest) |
110
+ | `sunpeak test` | Run unit + e2e tests |
111
+ | `sunpeak test --unit` | Run unit tests only (Vitest) |
112
+ | `sunpeak test --e2e` | Run e2e tests only (Playwright) |
113
+ | `sunpeak test --visual` | Run e2e tests with visual regression |
114
+ | `sunpeak test --visual --update` | Update visual regression baselines |
113
115
  | `sunpeak test --live` | Run live tests against real hosts |
116
+ | `sunpeak test init` | Scaffold test infrastructure into a project |
114
117
 
115
118
  **App framework** (for sunpeak projects):
116
119
 
@@ -176,7 +176,9 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
176
176
  }
177
177
 
178
178
  // Skip framework-internal test files (dev overlay tests are for sunpeak development, not user projects)
179
- if ((src.includes('/tests/e2e/') || src.includes('/tests/live/')) && name.startsWith('dev-')) {
179
+ // Skip visual.spec.ts it references specific resources and serves as a template/example.
180
+ // Users should write their own visual tests for their selected resources.
181
+ if ((src.includes('/tests/e2e/') || src.includes('/tests/live/')) && (name.startsWith('dev-') || name === 'visual.spec.ts')) {
180
182
  return false;
181
183
  }
182
184
 
@@ -5,11 +5,18 @@ import { join } from 'path';
5
5
  /**
6
6
  * sunpeak test — Run MCP server tests.
7
7
  *
8
- * sunpeak test Run e2e tests (Playwright)
9
- * sunpeak test init Scaffold test infrastructure
10
- * sunpeak test --unit Run unit tests (vitest)
11
- * sunpeak test --live Run live tests against real hosts
12
- * sunpeak test [pattern] Pass through to Playwright
8
+ * No flags: Run unit + e2e tests
9
+ * sunpeak test init Scaffold test infrastructure
10
+ * sunpeak test --unit Run unit tests (vitest)
11
+ * sunpeak test --e2e Run e2e tests (Playwright)
12
+ * sunpeak test --live Run live tests against real hosts
13
+ * sunpeak test --visual Run e2e tests with visual regression comparison
14
+ * sunpeak test --visual --update Update visual regression baselines
15
+ * sunpeak test [pattern] Pass through to the relevant runner
16
+ *
17
+ * Flags are additive: --unit --e2e --live runs all three.
18
+ * --visual implies --e2e and enables screenshot comparison.
19
+ * --update implies --visual.
13
20
  */
14
21
  export async function runTest(args) {
15
22
  // Handle `sunpeak test init` subcommand
@@ -20,58 +27,112 @@ export async function runTest(args) {
20
27
  }
21
28
 
22
29
  const isUnit = args.includes('--unit');
30
+ const isE2e = args.includes('--e2e');
23
31
  const isLive = args.includes('--live');
24
- const filteredArgs = args.filter((a) => a !== '--unit' && a !== '--live');
32
+ let isVisual = args.includes('--visual');
33
+ const isUpdate = args.includes('--update');
34
+ const filteredArgs = args.filter(
35
+ (a) => !['--unit', '--e2e', '--live', '--visual', '--update'].includes(a)
36
+ );
25
37
 
26
- if (isUnit) {
27
- // Run vitest
28
- const child = spawn('pnpm', ['exec', 'vitest', 'run', ...filteredArgs], {
29
- stdio: 'inherit',
30
- env: { ...process.env },
38
+ // --update implies --visual (no point updating without enabling visual)
39
+ if (isUpdate) isVisual = true;
40
+
41
+ const hasAnyScope = isUnit || isE2e || isLive || isVisual;
42
+
43
+ // When extra args are present (file patterns, etc.) and no scope flags given,
44
+ // default to e2e only — passing Playwright file patterns to vitest would fail.
45
+ const hasExtraArgs = filteredArgs.length > 0;
46
+
47
+ // Determine which suites to run.
48
+ // No scope flags → unit + e2e (unless extra args narrow to e2e).
49
+ // --visual implies e2e.
50
+ const runUnit = hasAnyScope ? isUnit : !hasExtraArgs;
51
+ const runE2e = hasAnyScope ? (isE2e || isVisual) : true;
52
+ const runLive = isLive;
53
+
54
+ const results = [];
55
+
56
+ if (runUnit) {
57
+ const code = await runChild('pnpm', ['exec', 'vitest', 'run', ...filteredArgs]);
58
+ results.push({ suite: 'unit', code });
59
+ }
60
+
61
+ if (runE2e) {
62
+ const code = await runPlaywright(filteredArgs, {
63
+ configCandidates: [
64
+ 'playwright.config.ts',
65
+ 'playwright.config.js',
66
+ 'sunpeak.config.ts',
67
+ 'sunpeak.config.js',
68
+ ],
69
+ visual: isVisual,
70
+ updateSnapshots: isVisual && isUpdate,
31
71
  });
32
- child.on('exit', (code) => process.exit(code ?? 1));
33
- return;
72
+ results.push({ suite: 'e2e', code });
34
73
  }
35
74
 
36
- // Find the appropriate Playwright config
37
- let configArgs = [];
38
- if (isLive) {
39
- const liveConfig = findConfig([
40
- 'tests/live/playwright.config.ts',
41
- 'tests/live/playwright.config.js',
42
- ]);
43
- if (liveConfig) {
44
- configArgs = ['--config', liveConfig];
45
- } else {
46
- console.error('No live test config found at tests/live/playwright.config.ts');
47
- process.exit(1);
48
- }
49
- } else {
50
- // Default: e2e tests — look for config in standard locations
51
- const e2eConfig = findConfig([
52
- 'playwright.config.ts',
53
- 'playwright.config.js',
54
- 'sunpeak.config.ts',
55
- 'sunpeak.config.js',
56
- ]);
57
- if (e2eConfig) {
58
- configArgs = ['--config', e2eConfig];
59
- }
60
- // If no config found, let Playwright use its defaults
75
+ if (runLive) {
76
+ const code = await runPlaywright(filteredArgs, {
77
+ configCandidates: [
78
+ 'tests/live/playwright.config.ts',
79
+ 'tests/live/playwright.config.js',
80
+ ],
81
+ configRequired: true,
82
+ configErrorMessage: 'No live test config found at tests/live/playwright.config.ts',
83
+ });
84
+ results.push({ suite: 'live', code });
85
+ }
86
+
87
+ // Exit with the first non-zero code, or 0 if all passed
88
+ const failed = results.find((r) => r.code !== 0);
89
+ process.exit(failed ? failed.code : 0);
90
+ }
91
+
92
+ /**
93
+ * Spawn a child process and return its exit code.
94
+ */
95
+ function runChild(command, args, env) {
96
+ return new Promise((resolve) => {
97
+ const child = spawn(command, args, {
98
+ stdio: 'inherit',
99
+ env: { ...process.env, ...env },
100
+ });
101
+ child.on('error', () => resolve(1));
102
+ child.on('exit', (code) => resolve(code ?? 1));
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Run Playwright and return the exit code.
108
+ */
109
+ function runPlaywright(args, options = {}) {
110
+ const {
111
+ configCandidates = [],
112
+ configRequired = false,
113
+ configErrorMessage,
114
+ visual = false,
115
+ updateSnapshots = false,
116
+ } = options;
117
+
118
+ const config = findConfig(configCandidates);
119
+
120
+ if (!config && configRequired) {
121
+ console.error(configErrorMessage);
122
+ return Promise.resolve(1);
61
123
  }
62
124
 
63
- const child = spawn(
125
+ const configArgs = config ? ['--config', config] : [];
126
+ const extraArgs = updateSnapshots ? ['--update-snapshots'] : [];
127
+
128
+ return runChild(
64
129
  'pnpm',
65
- ['exec', 'playwright', 'test', ...configArgs, ...filteredArgs],
130
+ ['exec', 'playwright', 'test', ...configArgs, ...extraArgs, ...args],
66
131
  {
67
- stdio: 'inherit',
68
- env: {
69
- ...process.env,
70
- SUNPEAK_DEV_OVERLAY: process.env.SUNPEAK_DEV_OVERLAY ?? 'false',
71
- },
132
+ SUNPEAK_DEV_OVERLAY: process.env.SUNPEAK_DEV_OVERLAY ?? 'false',
133
+ ...(visual ? { SUNPEAK_VISUAL: 'true' } : {}),
72
134
  }
73
135
  );
74
- child.on('exit', (code) => process.exit(code ?? 1));
75
136
  }
76
137
 
77
138
  function findConfig(candidates) {
@@ -1,3 +1,5 @@
1
+ import type { VisualConfig } from '../test/test-config.d.mts';
2
+
1
3
  export interface InspectConfigOptions {
2
4
  /** MCP server URL or stdio command string (required) */
3
5
  server: string;
@@ -11,6 +13,8 @@ export interface InspectConfigOptions {
11
13
  name?: string;
12
14
  /** Additional Playwright `use` options */
13
15
  use?: Record<string, unknown>;
16
+ /** Visual regression testing configuration */
17
+ visual?: VisualConfig;
14
18
  }
15
19
 
16
20
  /**
@@ -36,6 +36,7 @@ export function defineInspectConfig(options) {
36
36
  hosts = ['chatgpt', 'claude'],
37
37
  name,
38
38
  use: userUse,
39
+ visual,
39
40
  } = options;
40
41
 
41
42
  if (!server) {
@@ -60,6 +61,7 @@ export function defineInspectConfig(options) {
60
61
  testDir,
61
62
  port,
62
63
  use: userUse,
64
+ visual,
63
65
  webServer: {
64
66
  command,
65
67
  healthUrl: `http://localhost:${port}/health`,
@@ -17,7 +17,10 @@ import { getPortSync } from '../get-port.mjs';
17
17
  * @param {string} [options.globalSetup] - Global setup file path
18
18
  * @returns {import('@playwright/test').PlaywrightTestConfig}
19
19
  */
20
- export function createBaseConfig({ hosts, testDir, webServer, port, use, globalSetup }) {
20
+ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalSetup, visual }) {
21
+ // Separate snapshot path from other visual options passed to expect.toHaveScreenshot
22
+ const { snapshotPathTemplate, ...toHaveScreenshotDefaults } = visual ?? {};
23
+
21
24
  return {
22
25
  ...(globalSetup ? { globalSetup } : {}),
23
26
  testDir,
@@ -27,6 +30,18 @@ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalS
27
30
  // Limit workers to avoid overwhelming the double-iframe sandbox proxy.
28
31
  workers: process.env.CI ? 1 : 2,
29
32
  reporter: 'list',
33
+ // Only override snapshot path when visual config is provided, to avoid
34
+ // changing Playwright's default for projects that don't use visual testing.
35
+ ...(visual
36
+ ? {
37
+ snapshotPathTemplate:
38
+ snapshotPathTemplate ??
39
+ '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
40
+ }
41
+ : {}),
42
+ ...(Object.keys(toHaveScreenshotDefaults).length > 0
43
+ ? { expect: { toHaveScreenshot: toHaveScreenshotDefaults } }
44
+ : {}),
30
45
  use: {
31
46
  baseURL: `http://localhost:${port}`,
32
47
  trace: 'on-first-retry',
@@ -14,6 +14,26 @@ export interface ServerConfig {
14
14
  env?: Record<string, string>;
15
15
  }
16
16
 
17
+ /**
18
+ * Visual regression testing configuration.
19
+ *
20
+ * All fields except `snapshotPathTemplate` are forwarded to Playwright's
21
+ * `expect.toHaveScreenshot` config. See Playwright docs for the full set
22
+ * of options (threshold, maxDiffPixelRatio, maxDiffPixels, animations, etc.).
23
+ */
24
+ export interface VisualConfig {
25
+ /** Snapshot directory path template. Default: '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}'. */
26
+ snapshotPathTemplate?: string;
27
+ /** Pixel comparison threshold (0-1). */
28
+ threshold?: number;
29
+ /** Maximum allowed ratio of differing pixels (0-1). */
30
+ maxDiffPixelRatio?: number;
31
+ /** Absolute count of allowed different pixels. */
32
+ maxDiffPixels?: number;
33
+ /** Any other Playwright toHaveScreenshot options applied as project-wide defaults. */
34
+ [key: string]: unknown;
35
+ }
36
+
17
37
  /**
18
38
  * Configuration options for sunpeak test config.
19
39
  */
@@ -33,6 +53,8 @@ export interface TestConfigOptions {
33
53
  globalSetup?: string;
34
54
  /** Additional Playwright `use` options. */
35
55
  use?: Record<string, unknown>;
56
+ /** Visual regression testing configuration. */
57
+ visual?: VisualConfig;
36
58
  }
37
59
 
38
60
  /**
@@ -41,6 +41,7 @@ export function defineConfig(options = {}) {
41
41
  simulationsDir,
42
42
  globalSetup,
43
43
  use: userUse,
44
+ visual,
44
45
  } = options;
45
46
 
46
47
  const { port, sandboxPort } = resolvePorts();
@@ -67,6 +68,7 @@ export function defineConfig(options = {}) {
67
68
  port,
68
69
  use: userUse,
69
70
  globalSetup,
71
+ visual,
70
72
  webServer: {
71
73
  command,
72
74
  healthUrl: `http://localhost:${port}/health`,
@@ -1,4 +1,11 @@
1
- import type { Page, FrameLocator, TestType, Expect } from '@playwright/test';
1
+ import type {
2
+ Page,
3
+ FrameLocator,
4
+ Locator,
5
+ TestType,
6
+ Expect,
7
+ PageAssertionsToHaveScreenshotOptions,
8
+ } from '@playwright/test';
2
9
 
3
10
  /**
4
11
  * Result from calling an MCP tool via the inspector.
@@ -31,6 +38,22 @@ export interface CallToolOptions {
31
38
  [key: string]: unknown;
32
39
  }
33
40
 
41
+ /**
42
+ * Options for screenshot().
43
+ *
44
+ * Extends Playwright's toHaveScreenshot() options with sunpeak-specific
45
+ * `target` and `element` fields. All standard Playwright options (threshold,
46
+ * maxDiffPixelRatio, maxDiffPixels, mask, maskColor, animations, caret,
47
+ * fullPage, clip, scale, stylePath, omitBackground, timeout, etc.)
48
+ * are passed through directly.
49
+ */
50
+ export interface ScreenshotOptions extends PageAssertionsToHaveScreenshotOptions {
51
+ /** What to screenshot: 'app' (inner iframe content) or 'page' (full inspector). Default: 'app'. */
52
+ target?: 'app' | 'page';
53
+ /** Specific locator to screenshot instead of the default target. */
54
+ element?: Locator;
55
+ }
56
+
34
57
  /**
35
58
  * MCP test fixture for testing MCP servers via the inspector.
36
59
  */
@@ -66,6 +89,16 @@ export interface McpFixture {
66
89
 
67
90
  /** Change the display mode via the sidebar buttons. */
68
91
  setDisplayMode(mode: 'inline' | 'pip' | 'fullscreen'): Promise<void>;
92
+
93
+ /**
94
+ * Take a screenshot and compare against a baseline.
95
+ * Only performs the comparison when visual testing is enabled
96
+ * (`sunpeak test --visual`). Silently skips otherwise.
97
+ *
98
+ * @param name - Snapshot name (auto-generated from test title if omitted)
99
+ * @param options - Screenshot and comparison options
100
+ */
101
+ screenshot(name?: string, options?: ScreenshotOptions): Promise<void>;
69
102
  }
70
103
 
71
104
  /**
@@ -180,6 +180,49 @@ const test = base.extend({
180
180
  // Wait for display mode transition
181
181
  await page.waitForTimeout(500);
182
182
  },
183
+
184
+ /**
185
+ * Take a screenshot and compare against a baseline.
186
+ * Only performs the comparison when visual testing is enabled
187
+ * (`sunpeak test --visual`). Silently skips otherwise, so tests
188
+ * that include screenshot() calls still pass during normal runs.
189
+ *
190
+ * Accepts all Playwright toHaveScreenshot() options (threshold,
191
+ * maxDiffPixelRatio, maxDiffPixels, mask, animations, caret,
192
+ * fullPage, clip, scale, stylePath, etc.) and passes them through.
193
+ *
194
+ * @param {string} [name] - Snapshot name (auto-generated from test title if omitted)
195
+ * @param {Object} [options] - Screenshot and comparison options
196
+ * @param {'app' | 'page'} [options.target='app'] - What to screenshot
197
+ * @param {import('@playwright/test').Locator} [options.element] - Specific locator to screenshot
198
+ */
199
+ async screenshot(name, options = {}) {
200
+ if (process.env.SUNPEAK_VISUAL !== 'true') return;
201
+
202
+ // Support screenshot(options) without a name
203
+ if (typeof name === 'object' && name !== null) {
204
+ options = name;
205
+ name = undefined;
206
+ }
207
+
208
+ const { target = 'app', element, ...playwrightOptions } = options;
209
+
210
+ let locator;
211
+ if (element) {
212
+ locator = element;
213
+ } else if (target === 'page') {
214
+ locator = page.locator('#root');
215
+ } else {
216
+ locator = page.frameLocator('iframe').frameLocator('iframe').locator('body');
217
+ }
218
+
219
+ const fullName = name && !name.endsWith('.png') ? `${name}.png` : name;
220
+ const args = fullName
221
+ ? [fullName, playwrightOptions]
222
+ : [playwrightOptions];
223
+
224
+ await expect(locator).toHaveScreenshot(...args);
225
+ },
183
226
  };
184
227
 
185
228
  await use(fixture);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "Inspector, testing framework, and app framework for MCP Apps.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -17,9 +17,12 @@ That's it! Edit the resource files in [./src/resources/](./src/resources/) to bu
17
17
  **Testing:**
18
18
 
19
19
  ```bash
20
- sunpeak test # Run e2e tests against the inspector.
21
- sunpeak test --unit # Run unit tests with Vitest.
22
- sunpeak test --live # Run live tests against real ChatGPT.
20
+ sunpeak test # Run unit + e2e tests.
21
+ sunpeak test --unit # Run unit tests only (Vitest).
22
+ sunpeak test --e2e # Run e2e tests only (Playwright).
23
+ sunpeak test --visual # Run e2e tests with visual regression.
24
+ sunpeak test --visual --update # Update visual regression baselines.
25
+ sunpeak test --live # Run live tests against real ChatGPT.
23
26
  ```
24
27
 
25
28
  **Development and production:**
@@ -12,5 +12,5 @@
12
12
  }
13
13
  },
14
14
  "name": "albums",
15
- "uri": "ui://albums-mnnh0y9o"
15
+ "uri": "ui://albums-mnnqsot1"
16
16
  }
@@ -12,5 +12,5 @@
12
12
  }
13
13
  },
14
14
  "name": "carousel",
15
- "uri": "ui://carousel-mnnh0y9o"
15
+ "uri": "ui://carousel-mnnqsot1"
16
16
  }
@@ -18,5 +18,5 @@
18
18
  }
19
19
  },
20
20
  "name": "map",
21
- "uri": "ui://map-mnnh0y9o"
21
+ "uri": "ui://map-mnnqsot1"
22
22
  }
@@ -12,5 +12,5 @@
12
12
  }
13
13
  },
14
14
  "name": "review",
15
- "uri": "ui://review-mnnh0y9o"
15
+ "uri": "ui://review-mnnqsot1"
16
16
  }
@@ -7,9 +7,11 @@
7
7
  "dev": "sunpeak dev",
8
8
  "build": "sunpeak build",
9
9
  "start": "sunpeak start",
10
- "test": "vitest run",
11
- "test:e2e": "playwright test",
12
- "test:live": "playwright test --config tests/live/playwright.config.ts"
10
+ "test": "sunpeak test",
11
+ "test:unit": "sunpeak test --unit",
12
+ "test:e2e": "sunpeak test --e2e",
13
+ "test:visual": "sunpeak test --visual",
14
+ "test:live": "sunpeak test --live"
13
15
  },
14
16
  "dependencies": {
15
17
  "clsx": "^2.1.1",
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
@@ -0,0 +1,36 @@
1
+ import { test, expect } from 'sunpeak/test';
2
+
3
+ // Visual regression tests. Screenshot comparisons only run with `sunpeak test --visual`.
4
+ // Update baselines with `sunpeak test --visual --update`.
5
+
6
+ test('albums renders correctly in light mode', async ({ mcp }) => {
7
+ const result = await mcp.callTool('show-albums', {}, { theme: 'light' });
8
+ const app = result.app();
9
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();
10
+
11
+ await mcp.screenshot('albums-light');
12
+ });
13
+
14
+ test('albums renders correctly in dark mode', async ({ mcp }) => {
15
+ const result = await mcp.callTool('show-albums', {}, { theme: 'dark' });
16
+ const app = result.app();
17
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();
18
+
19
+ await mcp.screenshot('albums-dark');
20
+ });
21
+
22
+ test('albums renders correctly in fullscreen', async ({ mcp }) => {
23
+ const result = await mcp.callTool('show-albums', {}, { displayMode: 'fullscreen' });
24
+ const app = result.app();
25
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();
26
+
27
+ await mcp.screenshot('albums-fullscreen');
28
+ });
29
+
30
+ test('full page renders correctly', async ({ mcp }) => {
31
+ const result = await mcp.callTool('show-albums', {}, { theme: 'light' });
32
+ const app = result.app();
33
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();
34
+
35
+ await mcp.screenshot('albums-page-light', { target: 'page', maxDiffPixelRatio: 0.02 });
36
+ });