openspec-playwright 0.3.42 → 0.3.44

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.
@@ -1,19 +1,18 @@
1
1
  /**
2
- * Check if Playwright MCP server is installed in Claude Code.
3
- * Returns true if "playwright" appears in `claude mcp list` output.
2
+ * Shared MCP (Model Context Protocol) utilities.
4
3
  *
5
- * Note: `claude mcp list` may exit non-zero if another MCP server is
6
- * pending approval or unhealthy, while still printing the list. In that
7
- * case, read stdout/stderr from the thrown error before returning false.
4
+ * Dispatches Playwright MCP install/remove to the right editor adapter.
5
+ * Each adapter handles its own install mechanism (`claude mcp add` vs
6
+ * editing `opencode.jsonc`), so this layer just routes the call and
7
+ * prints status messages.
8
8
  */
9
- export declare function isPlaywrightMcpInstalled(): boolean;
10
- /**
11
- * Ensure Playwright MCP server is installed globally.
12
- * Prints status messages. Throws on failure.
13
- */
14
- export declare function ensurePlaywrightMcp(): void;
15
- /**
16
- * Remove Playwright MCP server from Claude Code.
17
- * Prints status messages. Does not throw if already absent.
18
- */
19
- export declare function removePlaywrightMcp(): void;
9
+ import { type EditorAdapter } from "../commands/editors.js";
10
+ /** Check if the named MCP server is installed in this editor. */
11
+ export declare function isMcpInstalled(adapter: EditorAdapter, serverName: string): boolean;
12
+ /** Install an MCP server in this editor. Throws on failure. */
13
+ export declare function ensureMcp(adapter: EditorAdapter, serverName: string, command: string[]): void;
14
+ /** Remove an MCP server from this editor. Does not throw if missing. */
15
+ export declare function removeMcp(adapter: EditorAdapter, serverName: string): void;
16
+ export declare function isPlaywrightMcpInstalled(adapter: EditorAdapter): boolean;
17
+ export declare function ensurePlaywrightMcp(adapter: EditorAdapter): void;
18
+ export declare function removePlaywrightMcp(adapter: EditorAdapter): void;
@@ -1,81 +1,45 @@
1
- /**
2
- * Shared MCP (Model Context Protocol) utilities.
3
- * Centralizes claude mcp check / install / remove logic.
4
- */
5
- import { execFileSync } from "node:child_process";
6
- import { TIMEOUT } from "./constants.js";
7
- import { needsShell } from "./platform.js";
8
- function outputIncludesPlaywright(output) {
9
- return String(output ?? "").includes("playwright");
1
+ /** Check if the named MCP server is installed in this editor. */
2
+ export function isMcpInstalled(adapter, serverName) {
3
+ return adapter.isMcpInstalled(process.cwd(), serverName);
10
4
  }
11
- /**
12
- * Check if Playwright MCP server is installed in Claude Code.
13
- * Returns true if "playwright" appears in `claude mcp list` output.
14
- *
15
- * Note: `claude mcp list` may exit non-zero if another MCP server is
16
- * pending approval or unhealthy, while still printing the list. In that
17
- * case, read stdout/stderr from the thrown error before returning false.
18
- */
19
- export function isPlaywrightMcpInstalled() {
20
- try {
21
- const output = execFileSync("claude", ["mcp", "list"], {
22
- encoding: "utf-8",
23
- timeout: TIMEOUT.MCP_LIST,
24
- stdio: ["pipe", "pipe", "pipe"],
25
- shell: needsShell,
26
- });
27
- return outputIncludesPlaywright(output);
28
- }
29
- catch (err) {
30
- const e = err;
31
- return outputIncludesPlaywright(e.stdout) || outputIncludesPlaywright(e.stderr);
32
- }
33
- }
34
- /**
35
- * Ensure Playwright MCP server is installed globally.
36
- * Prints status messages. Throws on failure.
37
- */
38
- export function ensurePlaywrightMcp() {
39
- if (isPlaywrightMcpInstalled()) {
40
- console.log(" ✓ Playwright MCP already installed");
5
+ /** Install an MCP server in this editor. Throws on failure. */
6
+ export function ensureMcp(adapter, serverName, command) {
7
+ if (isMcpInstalled(adapter, serverName)) {
8
+ console.log(` ✓ ${adapter.label}: ${serverName} MCP already installed`);
41
9
  return;
42
10
  }
43
11
  try {
44
- execFileSync("claude", ["mcp", "add", "playwright", "npx", "@playwright/mcp@latest"], {
45
- encoding: "utf-8",
46
- timeout: TIMEOUT.MCP_LIST,
47
- stdio: ["pipe", "pipe", "pipe"],
48
- shell: needsShell,
49
- });
50
- console.log(" ✓ Playwright MCP installed globally");
12
+ adapter.installMcp(process.cwd(), serverName, command);
13
+ console.log(` ✓ ${adapter.label}: ${serverName} MCP installed`);
51
14
  }
52
- catch {
53
- console.warn("Failed to install Playwright MCP");
54
- console.log(" Run manually: claude mcp add playwright npx @playwright/mcp@latest");
55
- throw new Error("MCP installation failed");
15
+ catch (err) {
16
+ console.warn(`${adapter.label}: failed to install ${serverName} MCP`);
17
+ throw err;
56
18
  }
57
19
  }
58
- /**
59
- * Remove Playwright MCP server from Claude Code.
60
- * Prints status messages. Does not throw if already absent.
61
- */
62
- export function removePlaywrightMcp() {
63
- if (!isPlaywrightMcpInstalled()) {
64
- console.log(" ✓ Playwright MCP not installed (nothing to remove)");
20
+ /** Remove an MCP server from this editor. Does not throw if missing. */
21
+ export function removeMcp(adapter, serverName) {
22
+ if (!isMcpInstalled(adapter, serverName)) {
23
+ console.log(` - ${adapter.label}: ${serverName} MCP not installed (nothing to remove)`);
65
24
  return;
66
25
  }
67
26
  try {
68
- execFileSync("claude", ["mcp", "remove", "playwright"], {
69
- encoding: "utf-8",
70
- timeout: TIMEOUT.MCP_LIST,
71
- stdio: ["pipe", "pipe", "pipe"],
72
- shell: needsShell,
73
- });
74
- console.log(" ✓ Playwright MCP removed");
27
+ adapter.removeMcp(process.cwd(), serverName);
28
+ console.log(` ✓ ${adapter.label}: ${serverName} MCP removed`);
75
29
  }
76
30
  catch {
77
- console.warn("Failed to remove Playwright MCP");
78
- console.log(" Run manually: claude mcp remove playwright");
31
+ console.warn(`${adapter.label}: failed to remove ${serverName} MCP`);
79
32
  }
80
33
  }
34
+ // ─── Playwright MCP conveniences ────────────────────────────────────────
35
+ const PLAYWRIGHT_MCP_COMMAND = ["npx", "@playwright/mcp@latest"];
36
+ export function isPlaywrightMcpInstalled(adapter) {
37
+ return isMcpInstalled(adapter, "playwright");
38
+ }
39
+ export function ensurePlaywrightMcp(adapter) {
40
+ ensureMcp(adapter, "playwright", PLAYWRIGHT_MCP_COMMAND);
41
+ }
42
+ export function removePlaywrightMcp(adapter) {
43
+ removeMcp(adapter, "playwright");
44
+ }
81
45
  //# sourceMappingURL=mcp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/shared/mcp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,SAAS,wBAAwB,CAAC,MAAe;IAC/C,OAAO,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE;YACrD,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,KAAK,EAAE,UAAU;SAClB,CAAC,CAAC;QACH,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAA6C,CAAC;QACxD,OAAO,wBAAwB,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,wBAAwB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAClF,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,wBAAwB,EAAE,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACpD,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,YAAY,CACV,QAAQ,EACR,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,wBAAwB,CAAC,EAC7D;YACE,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,KAAK,EAAE,UAAU;SAClB,CACF,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,sEAAsE,CAAC,CAAC;QACpF,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC,wBAAwB,EAAE,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAC;QACpE,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE;YACtD,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,KAAK,EAAE,UAAU;SAClB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;QACpD,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/shared/mcp.ts"],"names":[],"mappings":"AAUA,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,OAAsB,EAAE,UAAkB;IACvE,OAAO,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC;AAC3D,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,SAAS,CACvB,OAAsB,EACtB,UAAkB,EAClB,OAAiB;IAEjB,IAAI,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU,wBAAwB,CAAC,CAAC;QACzE,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU,gBAAgB,CAAC,CAAC;IACnE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC,KAAK,uBAAuB,UAAU,MAAM,CAAC,CAAC;QAC1E,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,SAAS,CAAC,OAAsB,EAAE,UAAkB;IAClE,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;QACzC,OAAO,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU,wCAAwC,CAAC,CAAC;QACzF,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU,cAAc,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC,KAAK,sBAAsB,UAAU,MAAM,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED,2EAA2E;AAE3E,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAAC;AAEjE,MAAM,UAAU,wBAAwB,CAAC,OAAsB;IAC7D,OAAO,cAAc,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,sBAAsB,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,SAAS,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AACnC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspec-playwright",
3
- "version": "0.3.42",
3
+ "version": "0.3.44",
4
4
  "description": "OpenSpec + Playwright E2E verification setup tool for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,8 +22,9 @@
22
22
  "chalk": "^5.3.0",
23
23
  "commander": "^12.1.0",
24
24
  "glob": "^13.0.6",
25
+ "jsonc-parser": "^3.3.1",
25
26
  "playwright": "^1.50.0",
26
- "tar": "^7.5.13"
27
+ "tar": "^7.5.16"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/glob": "^8.1.0",
@@ -37,6 +38,9 @@
37
38
  "typescript": "^5.6.0",
38
39
  "vitest": "^4.1.2"
39
40
  },
41
+ "overrides": {
42
+ "vite": "^8.0.16"
43
+ },
40
44
  "engines": {
41
45
  "node": ">=20"
42
46
  },
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Page, Locator, expect } from '@playwright/test';
6
6
 
7
- const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
7
+ const BASE_URL = process.env.BASE_URL;
8
8
 
9
9
  export class BasePage {
10
10
  protected page: Page;
@@ -23,7 +23,7 @@ export class BasePage {
23
23
  path: string,
24
24
  options?: { waitUntil?: 'domcontentloaded' | 'load' | 'networkidle' | 'commit' },
25
25
  ) {
26
- const url = path.startsWith('http') ? path : `${BASE_URL}${path}`;
26
+ const url = path.startsWith('http') || !BASE_URL ? path : new URL(path, BASE_URL).toString();
27
27
  await this.page.goto(url, {
28
28
  waitUntil: options?.waitUntil ?? 'domcontentloaded',
29
29
  });
@@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
2
2
  import { readFileSync, existsSync, readdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
 
5
- // ─── Detect project root (where openspec/ lives) ───
5
+ // ─── Detect project root (where package.json lives) ───
6
6
  function findProjectRoot(start: string): string {
7
7
  let dir = start;
8
8
  for (let i = 0; i < 10; i++) {
@@ -23,7 +23,8 @@ function findNpmRoot(projectRoot: string, maxDepth = 5): string {
23
23
  if (existsSync(pkgPath)) {
24
24
  try {
25
25
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
26
- if (pkg.scripts?.dev || pkg.scripts?.start || pkg.scripts?.serve || pkg.scripts?.preview) {
26
+ const scripts = pkg.scripts ?? {};
27
+ if (scripts['dev:all'] || scripts.dev || scripts.start || scripts.serve || scripts.preview) {
27
28
  return dir;
28
29
  }
29
30
  } catch {}
@@ -41,41 +42,116 @@ function findNpmRoot(projectRoot: string, maxDepth = 5): string {
41
42
  return search(projectRoot, 0) ?? projectRoot;
42
43
  }
43
44
 
45
+ function parsePort(text: string): number | undefined {
46
+ const patterns = [
47
+ /(?:^|\s)(?:--port|-p)\s+([0-9]{2,5})(?:\s|$)/,
48
+ /(?:^|\s)--port=([0-9]{2,5})(?:\s|$)/,
49
+ /(?:^|\s)(?:PORT|VITE_PORT|PLAYWRIGHT_PORT|E2E_PORT)=([0-9]{2,5})(?:\s|$)/,
50
+ /port\s*:\s*([0-9]{2,5})/,
51
+ ];
52
+ for (const pattern of patterns) {
53
+ const match = text.match(pattern);
54
+ if (match) {
55
+ const port = Number(match[1]);
56
+ if (port > 0 && port <= 65535) return port;
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ function parseEnvPort(content: string): number | undefined {
63
+ const lines = content.split(/\r?\n/);
64
+ for (const key of ['PLAYWRIGHT_PORT', 'E2E_PORT', 'VITE_PORT', 'PORT']) {
65
+ for (const line of lines) {
66
+ const match = line.match(new RegExp(`^\\s*${key}\\s*=\\s*['\"]?([0-9]{2,5})['\"]?\\s*$`));
67
+ if (match) {
68
+ const port = Number(match[1]);
69
+ if (port > 0 && port <= 65535) return port;
70
+ }
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function detectPortFromEnv(): number | undefined {
77
+ for (const key of ['PLAYWRIGHT_PORT', 'E2E_PORT', 'VITE_PORT', 'PORT']) {
78
+ const value = process.env[key];
79
+ if (!value) continue;
80
+ const port = Number(value);
81
+ if (Number.isInteger(port) && port > 0 && port <= 65535) return port;
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ function detectPortFromEnvFiles(npmRoot: string): number | undefined {
87
+ for (const file of ['.env.local', '.env.development', '.env']) {
88
+ const path = join(npmRoot, file);
89
+ if (!existsSync(path)) continue;
90
+ const port = parseEnvPort(readFileSync(path, 'utf-8'));
91
+ if (port) return port;
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function detectVitePort(npmRoot: string): number | undefined {
97
+ for (const file of ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs', 'vite.config.cjs']) {
98
+ const path = join(npmRoot, file);
99
+ if (!existsSync(path)) continue;
100
+ const port = parsePort(readFileSync(path, 'utf-8'));
101
+ if (port) return port;
102
+ }
103
+ return undefined;
104
+ }
105
+
106
+ function frameworkDefaultPort(pkg: Record<string, any>, command = ''): number | undefined {
107
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
108
+ if (command.includes('vite') || deps.vite) return 5173;
109
+ if (command.includes('astro') || deps.astro) return 4321;
110
+ if (command.includes('next') || deps.next) return 3000;
111
+ if (command.includes('nuxt') || deps.nuxt) return 3000;
112
+ return undefined;
113
+ }
114
+
44
115
  const projectRoot = findProjectRoot(process.cwd());
45
116
  const npmRoot = findNpmRoot(projectRoot);
117
+ const npmPkg = join(npmRoot, 'package.json');
118
+ const pkg = existsSync(npmPkg) ? JSON.parse(readFileSync(npmPkg, 'utf-8')) : {};
119
+ const scripts = pkg.scripts ?? {};
120
+
121
+ // ─── Dev command: detect from the npm project ───
122
+ const scriptName = scripts['dev:all']
123
+ ? 'dev:all'
124
+ : scripts.dev
125
+ ? 'dev'
126
+ : scripts.start
127
+ ? 'start'
128
+ : scripts.serve
129
+ ? 'serve'
130
+ : scripts.preview
131
+ ? 'preview'
132
+ : 'dev';
133
+ let devCmd = `npm run ${scriptName}`;
134
+ if (npmRoot !== projectRoot) {
135
+ devCmd = `cd "${npmRoot}" && ${devCmd}`;
136
+ }
46
137
 
47
- // ─── BASE_URL: prefer env, then seed.spec.ts, then default ───
138
+ // ─── BASE_URL: prefer env, then detected port, then seed.spec.ts, then default ───
48
139
  const seedSpec = join(projectRoot, 'tests', 'playwright', 'seed.spec.ts');
49
- let baseUrl = process.env.BASE_URL || 'http://localhost:3000';
50
- if (!process.env.BASE_URL && existsSync(seedSpec)) {
140
+ let baseUrl = process.env.BASE_URL;
141
+ if (!baseUrl) {
142
+ const scriptPort = scripts[scriptName] ? parsePort(scripts[scriptName]) : undefined;
143
+ const port = detectPortFromEnv() ?? scriptPort ?? detectVitePort(npmRoot) ?? detectPortFromEnvFiles(npmRoot) ?? frameworkDefaultPort(pkg, scripts[scriptName]);
144
+ if (port) baseUrl = `http://localhost:${port}`;
145
+ }
146
+ if (!baseUrl && existsSync(seedSpec)) {
51
147
  const content = readFileSync(seedSpec, 'utf-8');
52
148
  const m = content.match(/BASE_URL\s*=\s*process\.env\.BASE_URL\s*\|\|\s*['"]([^'"]+)['"]/);
53
- if (m) baseUrl = m[1];
54
- }
55
-
56
- // ─── Dev command: detect from the npm project ───
57
- let devCmd = 'npm run dev';
58
- const npmPkg = join(npmRoot, 'package.json');
59
- if (existsSync(npmPkg)) {
60
- const pkg = JSON.parse(readFileSync(npmPkg, 'utf-8'));
61
- const scripts = pkg.scripts ?? {};
62
- const scriptName = scripts['dev:all']
63
- ? 'dev:all'
64
- : scripts.dev
65
- ? 'dev'
66
- : scripts.start
67
- ? 'start'
68
- : scripts.serve
69
- ? 'serve'
70
- : scripts.preview
71
- ? 'preview'
72
- : 'dev';
73
- devCmd = `npm run ${scriptName}`;
74
- // Prefix with cd if npmRoot differs from projectRoot
75
- if (npmRoot !== projectRoot) {
76
- devCmd = `cd ${npmRoot} && ${devCmd}`;
149
+ if (m) {
150
+ const candidate = m[1];
151
+ if (candidate.startsWith('http://') || candidate.startsWith('https://')) baseUrl = candidate;
77
152
  }
78
153
  }
154
+ baseUrl ??= 'http://localhost:3000';
79
155
 
80
156
  const authStatePath = join(projectRoot, 'playwright', '.auth', 'user.json');
81
157
  const storageState = existsSync(authStatePath) ? authStatePath : undefined;
@@ -6,8 +6,8 @@ import { test, expect, Page, ConsoleMessage } from '@playwright/test';
6
6
  import { existsSync } from 'fs';
7
7
  import { BasePage } from './pages/BasePage';
8
8
 
9
- // Customize these for your application
10
- const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
9
+ // Prefer Playwright config's use.baseURL; env var is still supported for one-off overrides.
10
+ const BASE_URL = process.env.BASE_URL || '/';
11
11
 
12
12
  /**
13
13
  * Page Object Pattern - extends BasePage for shared utilities
@@ -69,7 +69,7 @@ test.describe('Application smoke tests', () => {
69
69
 
70
70
  test.describe('Environment validation', () => {
71
71
  test('BASE_URL responds 200', async ({ page }) => {
72
- const res = await page.request.get(`${BASE_URL}/`);
72
+ const res = await page.request.get(BASE_URL);
73
73
  expect(res.status(), `BASE_URL ${BASE_URL} returned ${res.status()}`).toBeLessThan(500);
74
74
  });
75
75