Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export default defineAuthConfig({
// If omitted, the CLI assumes your app is already running at `baseURL`.
webServer: {
command: "npm run dev",
// Optional: extra env for the server process (merged over process.env).
// env: { PORT: "3000" },
// Optional; defaults to baseURL when omitted.
// url: "http://127.0.0.1:3000/login",
},
Expand Down
1 change: 1 addition & 0 deletions examples/next-admin-auth/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ AUTH_ADMIN_EMAIL=admin@example.com
AUTH_ADMIN_PASSWORD=admin
AUTH_USER_EMAIL=user@example.com
AUTH_USER_PASSWORD=user
PLAYWRIGHT_KIT_EXAMPLE=next-admin-auth
3 changes: 1 addition & 2 deletions examples/next-admin-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dev": "next dev -p 3017",
"build": "next build",
"start": "next start -p 3017",
"auth:ensure": "playwright-kit auth ensure --dotenv",
"auth:ensure": "playwright-kit auth ensure",
"pretest": "npm run auth:ensure",
"test": "playwright test"
},
Expand All @@ -21,7 +21,6 @@
"@types/node": "^22.10.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"dotenv": "^16.4.7",
"playwright": "^1.50.1",
"typescript": "^5.7.3"
}
Expand Down
8 changes: 8 additions & 0 deletions examples/next-admin-auth/pages/api/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(_req: NextApiRequest, res: NextApiResponse): void {
res.status(200).json({
PLAYWRIGHT_KIT_EXAMPLE: process.env.PLAYWRIGHT_KIT_EXAMPLE ?? null,
});
}

3 changes: 3 additions & 0 deletions examples/next-admin-auth/playwright.auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { defineAuthConfig } from "@playwright-kit/auth";
import { loadEnvConfig } from "@next/env";
import type { Page } from "playwright";

loadEnvConfig(process.cwd());

const baseURL = "http://127.0.0.1:3017";

async function login(page: Page, email: string, password: string): Promise<void> {
Expand Down
8 changes: 8 additions & 0 deletions examples/next-admin-auth/tests/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test, expect } from "@playwright/test";

test("Next server exposes PLAYWRIGHT_KIT_EXAMPLE from env", async ({ request }) => {
const res = await request.get("/api/env");
expect(res.ok()).toBeTruthy();
await expect(res.json()).resolves.toEqual({ PLAYWRIGHT_KIT_EXAMPLE: "next-admin-auth" });
});

2 changes: 2 additions & 0 deletions packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export default defineAuthConfig({
// If omitted, the CLI assumes your app is already running at `baseURL`.
webServer: {
command: "npm run dev",
// Optional: extra env for the server process (merged over process.env).
// env: { PORT: "3000" },
// Optional; defaults to baseURL when omitted.
// url: "http://127.0.0.1:3000/login",
},
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/__tests__/all.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./envMapping.test";
import "./argParsing.test";
import "./configFindAndLoad.test";
import "./webServerEnv.test";
import "./lock.test";
31 changes: 30 additions & 1 deletion packages/auth/src/__tests__/configFindAndLoad.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test("loadAuthConfig allows webServer.url to be omitted when baseURL is set", as
[
"export default {",
" baseURL: 'http://127.0.0.1:3000',",
" webServer: { command: 'node', args: ['server.js'] },",
" webServer: { command: 'node', args: ['server.js'], env: { FOO: 'bar' } },",
" profiles: {",
" admin: {",
" validateUrl: '/',",
Expand All @@ -68,6 +68,7 @@ test("loadAuthConfig allows webServer.url to be omitted when baseURL is set", as
assert.equal(loaded.configFilePath, configPath);
assert.ok(loaded.config.webServer);
assert.equal(loaded.config.webServer.command, "node");
assert.deepEqual(loaded.config.webServer.env, { FOO: "bar" });
});

test("loadAuthConfig requires baseURL when webServer.url is omitted", async () => {
Expand Down Expand Up @@ -97,6 +98,34 @@ test("loadAuthConfig requires baseURL when webServer.url is omitted", async () =
);
});

test("loadAuthConfig rejects non-string webServer.env values", async () => {
const root = await makeTempDir();
const configPath = path.join(root, "playwright.auth.config.ts");
await fs.writeFile(
configPath,
[
"export default {",
" baseURL: 'http://127.0.0.1:3000',",
" webServer: { command: 'node', url: 'http://127.0.0.1:3000', env: { PORT: 3000 } },",
" profiles: {",
" admin: {",
" validateUrl: '/',",
" async login() {},",
" async validate() { return { ok: true }; },",
" },",
" },",
"};",
"",
].join("\n"),
"utf8",
);

await assert.rejects(
() => loadAuthConfig({ cwd: root }),
(error) => isUserError(error),
);
});

test("loadAuthConfig throws a user error when config is missing", async () => {
const root = await makeTempDir();
await assert.rejects(
Expand Down
61 changes: 61 additions & 0 deletions packages/auth/src/__tests__/webServerEnv.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";

import { withWebServer } from "../cli/webServer";

async function makeTempDir(): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), "playwright-kit-auth-webserver-env-"));
}

function getFreePort(): number {
return 20_000 + Math.floor(Math.random() * 10_000);
}

test("withWebServer passes webServer.env to the spawned process", async () => {
const root = await makeTempDir();
const port = getFreePort();
const serverPath = path.join(root, "server.mjs");
await fs.writeFile(
serverPath,
[
"import http from 'node:http';",
"",
"const port = Number(process.argv[2]);",
"const server = http.createServer((req, res) => {",
" if (req.url === '/env') {",
" res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });",
" res.end(JSON.stringify({ PLAYWRIGHT_KIT_EXAMPLE: process.env.PLAYWRIGHT_KIT_EXAMPLE ?? null }));",
" return;",
" }",
" res.writeHead(404);",
" res.end('not found');",
"});",
"",
"server.listen(port, '127.0.0.1');",
"process.on('SIGINT', () => server.close(() => process.exit(0)));",
"",
].join("\n"),
"utf8",
);

const url = `http://127.0.0.1:${port}/env`;
await withWebServer(
{
command: "node",
args: [serverPath, String(port)],
url,
timeoutMs: 10_000,
reuseExisting: false,
env: { PLAYWRIGHT_KIT_EXAMPLE: "vite-react-auth" },
},
async () => {
const res = await fetch(url);
assert.equal(res.status, 200);
const data: unknown = await res.json();
assert.deepEqual(data, { PLAYWRIGHT_KIT_EXAMPLE: "vite-react-auth" });
},
);
});
1 change: 1 addition & 0 deletions packages/auth/src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface WebServerArgs {
url: string;
timeoutMs: number;
reuseExisting: boolean;
env?: Record<string, string>;
}

export interface DotenvArgs {
Expand Down
38 changes: 19 additions & 19 deletions packages/auth/src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@ async function run(argv: string[]): Promise<number> {
const loaded = await loadAuthConfig({ cwd: process.cwd(), configPath: parsed.configPath });
console.log(`auth: config ${loaded.configFilePath}`);

const resolvedWebServer: WebServerArgs | undefined =
parsed.webServer ??
(loaded.config.webServer
? {
command: loaded.config.webServer.command,
args: loaded.config.webServer.args ?? [],
url:
loaded.config.webServer.url ??
loaded.config.baseURL ??
(() => {
throw createUserError(
`Auth config webServer.url is missing and baseURL is not set.`,
);
})(),
timeoutMs: loaded.config.webServer.timeoutMs ?? DEFAULT_WEB_SERVER_TIMEOUT_MS,
reuseExisting:
loaded.config.webServer.reuseExisting ?? DEFAULT_WEB_SERVER_REUSE_EXISTING,
}
: undefined);
const webServerFromConfig: WebServerArgs | undefined = loaded.config.webServer
? {
command: loaded.config.webServer.command,
args: loaded.config.webServer.args ?? [],
url:
loaded.config.webServer.url ??
loaded.config.baseURL ??
(() => {
throw createUserError(`Auth config webServer.url is missing and baseURL is not set.`);
})(),
timeoutMs: loaded.config.webServer.timeoutMs ?? DEFAULT_WEB_SERVER_TIMEOUT_MS,
reuseExisting: loaded.config.webServer.reuseExisting ?? DEFAULT_WEB_SERVER_REUSE_EXISTING,
env: loaded.config.webServer.env,
}
: undefined;

const resolvedWebServer: WebServerArgs | undefined = parsed.webServer
? { ...parsed.webServer, env: parsed.webServer.env ?? webServerFromConfig?.env }
: webServerFromConfig;

if (parsed.kind === "setup") {
const result = await withWebServer(resolvedWebServer, async () =>
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/cli/webServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,14 @@ export async function withWebServer<T>(
? [quoteCmdArg(command), ...webServer.args.map(quoteCmdArg)].join(" ")
: command;
const argsForSpawn = forceQuotedCommandLine ? [] : webServer.args;
const envForSpawn = webServer.env ? { ...process.env, ...webServer.env } : process.env;

let child: ReturnType<typeof spawn>;
try {
child = spawn(commandForSpawn, argsForSpawn, {
stdio: "inherit",
shell: useShell,
env: process.env,
env: envForSpawn,
detached: process.platform !== "win32",
windowsHide: true,
});
Expand Down
14 changes: 14 additions & 0 deletions packages/auth/src/config/loadAuthConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ function assertAuthConfig(config: unknown): asserts config is AuthConfig {
) {
throw createUserError(`Auth config "webServer.args" must be an array of strings.`);
}
if (config.webServer.env !== undefined) {
if (!isObject(config.webServer.env)) {
throw createUserError(
`Auth config "webServer.env" must be an object (key/value pairs) with string values.`,
);
}
for (const [key, value] of Object.entries(config.webServer.env)) {
if (typeof value !== "string") {
throw createUserError(
`Auth config "webServer.env.${key}" must be a string (got ${typeof value}).`,
);
}
}
}
}

if (
Expand Down
5 changes: 5 additions & 0 deletions packages/auth/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface AuthWebServerConfig {
url?: string;
timeoutMs?: number;
reuseExisting?: boolean;
/**
* Extra environment variables for the web server process.
* Merged as `{ ...process.env, ...env }`.
*/
env?: Record<string, string>;
}

export interface AuthCredentials {
Expand Down