@saucebase/laravel-playwright 1.0.0 → 2.1.0
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.
- package/README.md +70 -4
- package/dist/config.d.ts +42 -0
- package/dist/index.d.ts +12 -2
- package/dist/laravel-playwright.js +98 -35
- package/dist/laravel-playwright.umd.cjs +5 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -45,9 +45,10 @@ return [
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* The environments in which the testing endpoints are enabled.
|
|
48
|
-
* CAUTION:
|
|
48
|
+
* CAUTION: the endpoints run arbitrary PHP and SQL. Never enable them in
|
|
49
|
+
* production, nor in `local`, the environment your own dev server runs in.
|
|
49
50
|
*/
|
|
50
|
-
'environments' => ['
|
|
51
|
+
'environments' => ['e2e', 'testing'],
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Optional secret token to authenticate Playwright requests.
|
|
@@ -88,6 +89,53 @@ export default defineConfig<LaravelOptions>({
|
|
|
88
89
|
});
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
## 🏝️ A dedicated environment
|
|
93
|
+
|
|
94
|
+
Run the suite on its own server and database, so it never touches your dev app or its data. This works the same however you serve the app day to day: Herd, Valet, Sail, Docker or `artisan serve`.
|
|
95
|
+
|
|
96
|
+
**1. Create `.env.e2e`:**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
php artisan playwright:env
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This writes a copy of your `.env` with its own server and database. Laravel loads one env file, not one on top of another, so the copy keeps everything else your app needs (`APP_KEY`, secrets, mail). Only these change:
|
|
103
|
+
|
|
104
|
+
```dotenv
|
|
105
|
+
APP_ENV=e2e
|
|
106
|
+
APP_URL=http://localhost:8100
|
|
107
|
+
DB_CONNECTION=sqlite
|
|
108
|
+
DB_URL=
|
|
109
|
+
DB_DATABASE=database/e2e.sqlite
|
|
110
|
+
SESSION_DRIVER=file
|
|
111
|
+
CACHE_STORE=file
|
|
112
|
+
QUEUE_CONNECTION=sync
|
|
113
|
+
MAIL_MAILER=log
|
|
114
|
+
REDIS_PREFIX=<your prefix>e2e_
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Options: `php artisan playwright:env <environment> --url=http://localhost:9000 --force`. Edit the file afterwards for anything else the suite needs. Keep `.env.e2e` out of git like `.env`, and in CI, run the command after creating `.env`.
|
|
118
|
+
|
|
119
|
+
**2. Let Playwright start the server:**
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { defineConfig } from '@playwright/test';
|
|
123
|
+
import { laravelServer, type LaravelOptions } from '@saucebase/laravel-playwright';
|
|
124
|
+
|
|
125
|
+
export default defineConfig<LaravelOptions>({
|
|
126
|
+
use: { baseURL: 'http://localhost:8100' },
|
|
127
|
+
webServer: laravelServer(), // migrate, then php artisan serve --env=e2e on APP_URL's port
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`laravelServer({ env, host, workers, cwd, migrate })` returns a Playwright `webServer` entry. It serves the app on the port of `.env.e2e`'s `APP_URL` and waits on that URL, because an app that tells tenants apart by host answers 404 on any other. It migrates the database first (creating the SQLite file if needed), because an app that reads the database on every request, such as a tenant lookup at boot, can't be migrated over HTTP while its tables are missing. Pass `migrate: false` to skip that. It sets `APP_ENV` and `PHP_CLI_SERVER_WORKERS` (4 by default) for the server, and reuses a running one outside CI. It gives the server its own config, route and event caches (`bootstrap/cache/e2e/`). A config you cached with `config:cache` would otherwise skip every env file and give the server your dev database. It refuses to start if:
|
|
132
|
+
|
|
133
|
+
- `.env.e2e` is missing, because Laravel would then fall back to `.env`.
|
|
134
|
+
- `APP_URL` is missing or not `http:`. `artisan serve` has no TLS.
|
|
135
|
+
- The Playwright process already has another `APP_ENV`. The server inherits the process's environment, and real environment variables override any env file, so `import 'dotenv/config'` (which loads `.env`) would give the server your dev database. To read values from `.env.e2e` in the config, use `config({ path: '.env.e2e' })` from dotenv.
|
|
136
|
+
|
|
137
|
+
**Subdomains and multi-tenancy.** Chromium resolves every `*.localhost` to your machine, so `http://acme.localhost:8100` reaches the same server with no DNS or hosts-file setup. To use a real wildcard host instead (Herd or Valet `*.app.test`), skip `laravelServer()` and point `baseURL` at that host, served with `APP_ENV=e2e`.
|
|
138
|
+
|
|
91
139
|
## 🔒 Security
|
|
92
140
|
|
|
93
141
|
The package supports an optional shared secret to prevent unauthorized access to the testing endpoints.
|
|
@@ -205,7 +253,13 @@ test('example', async ({ laravel }) => {
|
|
|
205
253
|
|
|
206
254
|
## 🔄 Dynamic Configuration
|
|
207
255
|
|
|
208
|
-
You can update Laravel config for
|
|
256
|
+
You can update Laravel config for every subsequent request **of the same test**, until the test ends.
|
|
257
|
+
|
|
258
|
+
The state is kept per test. The `laravel` fixture gives each test an id and sends it in an `X-Playwright-Test` header, on its own calls and on the browser's requests to the app. The server applies a test's state only to requests that carry its id. Parallel tests never see or remove each other's config, time or boot functions, and requests without the header (your own browser, a queue worker) see none.
|
|
259
|
+
|
|
260
|
+
- Only requests to the app carry the header: the `baseURL` host and its subdomains. Third-party requests (Stripe, CDNs) are left alone, so no CORS preflight fails. Override the match with the `laravelIsAppUrl: (url: URL) => boolean` option.
|
|
261
|
+
- The header is added with `context.route()`. If you add your own route for app URLs, call `route.fallback()`, not `route.continue()`, so the header is still added.
|
|
262
|
+
- A context you create yourself (`browser.newContext()`) doesn't get the header.
|
|
209
263
|
|
|
210
264
|
```ts
|
|
211
265
|
import { test } from '@saucebase/laravel-playwright';
|
|
@@ -283,7 +337,19 @@ test('checkout with fake payment gateway', async ({ laravel, page }) => {
|
|
|
283
337
|
> **Why not use `callFunction` instead?**
|
|
284
338
|
> `callFunction` runs once and returns. `registerBootFunction` runs at the start of **every** request the browser makes during the test, so service bindings registered in the boot phase are in place for the full request lifecycle — including middleware, controllers, and event listeners.
|
|
285
339
|
|
|
286
|
-
> **Note:** The helper class only needs to exist in your `
|
|
340
|
+
> **Note:** The helper class only needs to exist in your `e2e` and `testing` environments. You can guard it with an environment check or keep it out of production deployments entirely.
|
|
341
|
+
|
|
342
|
+
## 🤖 Laravel Boost
|
|
343
|
+
|
|
344
|
+
The package ships a [Laravel Boost](https://laravel.com/docs/boost) guideline and a `laravel-playwright-development` skill, covering the e2e environment, per-test state, subdomains and common failures. Boost asks before including third-party packages: pick `saucebase/laravel-playwright` in `php artisan boost:install`, or add it to `packages` in `boost.json` and run `php artisan boost:update`.
|
|
345
|
+
|
|
346
|
+
## ⬆️ Upgrading from v1
|
|
347
|
+
|
|
348
|
+
- **The endpoints are off in `local`.** The default `environments` is now `['e2e', 'testing']`. Serve the suite with `APP_ENV=e2e` (see [A dedicated environment](#️-a-dedicated-environment)). If you published the config, update `environments` there.
|
|
349
|
+
- **Dynamic config, `travel()` and boot functions are per test.** State moved from `storage/laravel-playwright-config.json` to `storage/framework/playwright/<test-id>.json`, and only applies to requests that carry the test's `X-Playwright-Test` header. The fixture sends the header for you. If you call the endpoints from your own client, send it yourself; `dynamicConfig`, `travel` and `registerBootFunction` return 400 without it.
|
|
350
|
+
- `DynamicConfig::set()`, `get()`, `getAll()`, `load()` and `delete()` now take the test id as their first argument.
|
|
351
|
+
- **Artisan commands run from the application root**, like the CLI, rather than the web server's `public/`.
|
|
352
|
+
- Requires Laravel 12 or 13.
|
|
287
353
|
|
|
288
354
|
## 🙏 Credits
|
|
289
355
|
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface LaravelServerOptions {
|
|
2
|
+
/** The environment to serve; `.env.<env>` is loaded, and its APP_URL is where the app is served. @default 'e2e' */
|
|
3
|
+
env?: string;
|
|
4
|
+
/** The address to bind. @default '127.0.0.1' */
|
|
5
|
+
host?: string;
|
|
6
|
+
/** PHP_CLI_SERVER_WORKERS, so parallel tests are not served one request at a time. @default 4 */
|
|
7
|
+
workers?: number;
|
|
8
|
+
/** The application root. @default process.cwd() */
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Migrate before serving. An app that reads the database on every request (a
|
|
12
|
+
* tenant lookup at boot) cannot be migrated over HTTP while its tables are missing.
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
migrate?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A Playwright `webServer` entry serving the app in its own environment.
|
|
19
|
+
*
|
|
20
|
+
* The suite then never touches the developer's own server or database, whatever
|
|
21
|
+
* serves those (Herd, Valet, Sail, Docker or `artisan serve`): `.env.<env>` names
|
|
22
|
+
* its own database and APP_URL, and nothing else is shared.
|
|
23
|
+
*/
|
|
24
|
+
export declare function laravelServer(options?: LaravelServerOptions): {
|
|
25
|
+
command: string;
|
|
26
|
+
url: string;
|
|
27
|
+
cwd: string;
|
|
28
|
+
env: {
|
|
29
|
+
APP_ENV: string;
|
|
30
|
+
PHP_CLI_SERVER_WORKERS: string;
|
|
31
|
+
APP_CONFIG_CACHE: string;
|
|
32
|
+
APP_ROUTES_CACHE: string;
|
|
33
|
+
APP_EVENTS_CACHE: string;
|
|
34
|
+
};
|
|
35
|
+
reuseExistingServer: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Whether a request goes to the app: the base URL's host or one of its subdomains,
|
|
39
|
+
* on any port, so subdomain multi-tenancy is covered. Third parties are not, so the
|
|
40
|
+
* test header never turns their requests into failing CORS preflights.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isAppUrl(url: URL, baseURL: string | undefined): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { APIRequestContext } from '@playwright/test';
|
|
2
|
+
export { isAppUrl, laravelServer, type LaravelServerOptions } from './config.ts';
|
|
3
|
+
/** Ties a request to the test that made it, so its state applies to it alone. */
|
|
4
|
+
export declare const TEST_HEADER = "X-Playwright-Test";
|
|
2
5
|
export interface LaravelOptions {
|
|
3
6
|
/**
|
|
4
7
|
* The base URL to the endpoints
|
|
@@ -9,16 +12,24 @@ export interface LaravelOptions {
|
|
|
9
12
|
* Optional secret token sent as X-Playwright-Secret header
|
|
10
13
|
*/
|
|
11
14
|
laravelSecret: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Which browser requests go to the app and carry the test header.
|
|
17
|
+
* @default the base URL's host and its subdomains
|
|
18
|
+
*/
|
|
19
|
+
laravelIsAppUrl: ((url: URL) => boolean) | undefined;
|
|
12
20
|
}
|
|
13
21
|
interface LaravelFixtures {
|
|
14
22
|
laravel: Laravel;
|
|
23
|
+
/** Unique per test; the server keeps each test's config, time and boot functions apart by it. */
|
|
24
|
+
laravelTestId: string;
|
|
15
25
|
}
|
|
16
26
|
export declare const test: import('playwright/test').TestType<import('playwright/test').PlaywrightTestArgs & import('playwright/test').PlaywrightTestOptions & LaravelFixtures & LaravelOptions, import('playwright/test').PlaywrightWorkerArgs & import('playwright/test').PlaywrightWorkerOptions>;
|
|
17
27
|
export declare class Laravel {
|
|
18
28
|
private baseUrl;
|
|
19
29
|
private request;
|
|
20
30
|
private secret;
|
|
21
|
-
|
|
31
|
+
readonly testId: string;
|
|
32
|
+
constructor(baseUrl: string, request: APIRequestContext, secret?: string | undefined, testId?: string);
|
|
22
33
|
call<T = unknown>(endpoint: string, data?: object): Promise<T>;
|
|
23
34
|
artisan(command: string, parameters?: string[]): Promise<{
|
|
24
35
|
code: number;
|
|
@@ -49,4 +60,3 @@ export declare class Laravel {
|
|
|
49
60
|
registerBootFunction(func: string): Promise<unknown>;
|
|
50
61
|
tearDown(): Promise<unknown>;
|
|
51
62
|
}
|
|
52
|
-
export {};
|
|
@@ -1,60 +1,120 @@
|
|
|
1
|
-
import { test as
|
|
2
|
-
|
|
1
|
+
import { test as h } from "@playwright/test";
|
|
2
|
+
import { existsSync as p, readFileSync as v } from "node:fs";
|
|
3
|
+
import { join as u } from "node:path";
|
|
4
|
+
import { parseEnv as w } from "node:util";
|
|
5
|
+
function m(s = {}) {
|
|
6
|
+
const { env: t = "e2e", host: e = "127.0.0.1", workers: a = 4, cwd: r = process.cwd(), migrate: n = !0 } = s, o = u(r, `.env.${t}`);
|
|
7
|
+
if (!p(o))
|
|
8
|
+
throw new Error(`laravelServer: .env.${t} is missing in ${r}. Create it with \`php artisan playwright:env${t === "e2e" ? "" : ` ${t}`}\` (a copy of .env with its own server and database).`);
|
|
9
|
+
if (process.env.APP_ENV !== void 0 && process.env.APP_ENV !== t)
|
|
10
|
+
throw new Error(
|
|
11
|
+
`laravelServer: the Playwright process has APP_ENV=${process.env.APP_ENV}, so another env file was loaded and its values would override .env.${t}. Load .env.${t} instead (dotenv config({ path: '.env.${t}' })) or none.`
|
|
12
|
+
);
|
|
13
|
+
const i = w(v(o, "utf8")).APP_URL;
|
|
14
|
+
if (!i)
|
|
15
|
+
throw new Error(`laravelServer: set APP_URL in .env.${t}, e.g. http://localhost:8100.`);
|
|
16
|
+
const c = new URL(i);
|
|
17
|
+
if (c.protocol !== "http:")
|
|
18
|
+
throw new Error(`laravelServer: APP_URL in .env.${t} must be http: (artisan serve has no TLS), got ${i}.`);
|
|
19
|
+
return {
|
|
20
|
+
command: [
|
|
21
|
+
n && `php artisan migrate --env=${t} --force`,
|
|
22
|
+
// --no-reload: without it artisan serve ignores PHP_CLI_SERVER_WORKERS.
|
|
23
|
+
`php artisan serve --env=${t} --host=${e} --port=${c.port || 80} --no-reload`
|
|
24
|
+
].filter(Boolean).join(" && "),
|
|
25
|
+
url: c.origin,
|
|
26
|
+
cwd: r,
|
|
27
|
+
env: {
|
|
28
|
+
APP_ENV: t,
|
|
29
|
+
PHP_CLI_SERVER_WORKERS: String(a),
|
|
30
|
+
// A cached config skips every env file, so the developer's `config:cache`
|
|
31
|
+
// would hand this server (and its migrate) their database. Cached routes
|
|
32
|
+
// were built without the e2e endpoints. Laravel reads these paths from the
|
|
33
|
+
// environment, so the e2e server gets caches of its own.
|
|
34
|
+
APP_CONFIG_CACHE: `bootstrap/cache/${t}/config.php`,
|
|
35
|
+
APP_ROUTES_CACHE: `bootstrap/cache/${t}/routes.php`,
|
|
36
|
+
APP_EVENTS_CACHE: `bootstrap/cache/${t}/events.php`
|
|
37
|
+
},
|
|
38
|
+
reuseExistingServer: !process.env.CI
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function d(s, t) {
|
|
42
|
+
if (!t)
|
|
43
|
+
return !1;
|
|
44
|
+
const e = new URL(t).hostname;
|
|
45
|
+
return s.hostname === e || s.hostname.endsWith(`.${e}`);
|
|
46
|
+
}
|
|
47
|
+
const l = "X-Playwright-Test", g = h.extend({
|
|
3
48
|
laravelBaseUrl: [void 0, { option: !0 }],
|
|
4
49
|
laravelSecret: [void 0, { option: !0 }],
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
await
|
|
50
|
+
laravelIsAppUrl: [void 0, { option: !0 }],
|
|
51
|
+
laravelTestId: async ({}, s) => {
|
|
52
|
+
await s(crypto.randomUUID());
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Adds the test header to the browser's requests to the app only. `extraHTTPHeaders`
|
|
56
|
+
* would send it to third parties too, turning their requests into CORS preflights
|
|
57
|
+
* that fail. `fallback()` lets any route registered later still handle the request.
|
|
58
|
+
*/
|
|
59
|
+
context: async ({ context: s, baseURL: t, laravelIsAppUrl: e, laravelTestId: a }, r) => {
|
|
60
|
+
const n = e ?? ((o) => d(o, t));
|
|
61
|
+
await s.route(n, (o) => o.fallback({
|
|
62
|
+
headers: { ...o.request().headers(), [l]: a }
|
|
63
|
+
})), await r(s);
|
|
64
|
+
},
|
|
65
|
+
laravel: async ({ laravelBaseUrl: s, laravelSecret: t, laravelTestId: e, baseURL: a, request: r }, n) => {
|
|
66
|
+
const o = s || a + "/playwright", i = new f(o, r, t, e);
|
|
67
|
+
await n(i), await i.tearDown();
|
|
8
68
|
}
|
|
9
69
|
});
|
|
10
|
-
class
|
|
11
|
-
constructor(t,
|
|
12
|
-
this.baseUrl = t, this.request =
|
|
70
|
+
class f {
|
|
71
|
+
constructor(t, e, a = void 0, r = crypto.randomUUID()) {
|
|
72
|
+
this.baseUrl = t, this.request = e, this.secret = a, this.testId = r;
|
|
13
73
|
}
|
|
14
|
-
async call(t,
|
|
15
|
-
const
|
|
16
|
-
this.secret && (
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
74
|
+
async call(t, e = {}) {
|
|
75
|
+
const a = this.baseUrl.replace(/\/$/, "") + t, r = { Accept: "application/json", [l]: this.testId };
|
|
76
|
+
this.secret && (r["X-Playwright-Secret"] = this.secret);
|
|
77
|
+
const n = await this.request.post(a, { data: e, headers: r });
|
|
78
|
+
if (n.status() !== 200)
|
|
19
79
|
throw new Error(`
|
|
20
80
|
Failed to call Laravel ${t}.
|
|
21
|
-
Status: ${
|
|
22
|
-
Response: ${await
|
|
81
|
+
Status: ${n.status()}
|
|
82
|
+
Response: ${await n.text()}
|
|
23
83
|
`);
|
|
24
|
-
return await
|
|
84
|
+
return await n.json();
|
|
25
85
|
}
|
|
26
|
-
async artisan(t,
|
|
27
|
-
return await this.call("/artisan", { command: t, parameters:
|
|
86
|
+
async artisan(t, e = []) {
|
|
87
|
+
return await this.call("/artisan", { command: t, parameters: e });
|
|
28
88
|
}
|
|
29
89
|
async truncate(t = []) {
|
|
30
90
|
return await this.call("/truncate", { connections: t });
|
|
31
91
|
}
|
|
32
|
-
async factory(t,
|
|
33
|
-
return await this.call("/factory", { model: t, count:
|
|
92
|
+
async factory(t, e = {}, a) {
|
|
93
|
+
return await this.call("/factory", { model: t, count: a, attrs: e });
|
|
34
94
|
}
|
|
35
|
-
async query(t,
|
|
36
|
-
const { connection:
|
|
37
|
-
if (
|
|
95
|
+
async query(t, e = [], a = {}) {
|
|
96
|
+
const { connection: r = null, unprepared: n = !1 } = a;
|
|
97
|
+
if (n && e.length > 0)
|
|
38
98
|
throw new Error("Cannot use unprepared with bindings");
|
|
39
99
|
return await this.call("/query", {
|
|
40
100
|
query: t,
|
|
41
|
-
bindings:
|
|
42
|
-
connection:
|
|
43
|
-
unprepared:
|
|
101
|
+
bindings: e,
|
|
102
|
+
connection: r,
|
|
103
|
+
unprepared: n
|
|
44
104
|
});
|
|
45
105
|
}
|
|
46
|
-
async select(t,
|
|
47
|
-
const { connection:
|
|
48
|
-
return await this.call("/select", { query: t, bindings:
|
|
106
|
+
async select(t, e = {}, a = {}) {
|
|
107
|
+
const { connection: r = null } = a;
|
|
108
|
+
return await this.call("/select", { query: t, bindings: e, connection: r });
|
|
49
109
|
}
|
|
50
|
-
async callFunction(t,
|
|
51
|
-
return await this.call("/function", { function: t, args:
|
|
110
|
+
async callFunction(t, e = []) {
|
|
111
|
+
return await this.call("/function", { function: t, args: e });
|
|
52
112
|
}
|
|
53
113
|
/**
|
|
54
114
|
* Sets a laravel config value until tearDown is called (or the test ends)
|
|
55
115
|
*/
|
|
56
|
-
async config(t,
|
|
57
|
-
return await this.call("/dynamicConfig", { key: t, value:
|
|
116
|
+
async config(t, e) {
|
|
117
|
+
return await this.call("/dynamicConfig", { key: t, value: e });
|
|
58
118
|
}
|
|
59
119
|
/**
|
|
60
120
|
* Travel to a specific time
|
|
@@ -71,6 +131,9 @@ class l {
|
|
|
71
131
|
}
|
|
72
132
|
}
|
|
73
133
|
export {
|
|
74
|
-
|
|
75
|
-
|
|
134
|
+
f as Laravel,
|
|
135
|
+
l as TEST_HEADER,
|
|
136
|
+
d as isAppUrl,
|
|
137
|
+
m as laravelServer,
|
|
138
|
+
g as test
|
|
76
139
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
(function(
|
|
2
|
-
Failed to call Laravel ${
|
|
3
|
-
Status: ${
|
|
4
|
-
Response: ${await
|
|
5
|
-
`);return await
|
|
1
|
+
(function(n,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("@playwright/test"),require("node:fs"),require("node:path"),require("node:util")):typeof define=="function"&&define.amd?define(["exports","@playwright/test","node:fs","node:path","node:util"],l):(n=typeof globalThis<"u"?globalThis:n||self,l(n.LaravelPlaywright={},n.PlaywrightTest,n.node_fs,n.node_path,n.node_util))})(this,(function(n,l,p,d,f){"use strict";function y(i={}){const{env:e="e2e",host:t="127.0.0.1",workers:a=4,cwd:r=process.cwd(),migrate:s=!0}=i,o=d.join(r,`.env.${e}`);if(!p.existsSync(o))throw new Error(`laravelServer: .env.${e} is missing in ${r}. Create it with \`php artisan playwright:env${e==="e2e"?"":` ${e}`}\` (a copy of .env with its own server and database).`);if(process.env.APP_ENV!==void 0&&process.env.APP_ENV!==e)throw new Error(`laravelServer: the Playwright process has APP_ENV=${process.env.APP_ENV}, so another env file was loaded and its values would override .env.${e}. Load .env.${e} instead (dotenv config({ path: '.env.${e}' })) or none.`);const c=f.parseEnv(p.readFileSync(o,"utf8")).APP_URL;if(!c)throw new Error(`laravelServer: set APP_URL in .env.${e}, e.g. http://localhost:8100.`);const u=new URL(c);if(u.protocol!=="http:")throw new Error(`laravelServer: APP_URL in .env.${e} must be http: (artisan serve has no TLS), got ${c}.`);return{command:[s&&`php artisan migrate --env=${e} --force`,`php artisan serve --env=${e} --host=${t} --port=${u.port||80} --no-reload`].filter(Boolean).join(" && "),url:u.origin,cwd:r,env:{APP_ENV:e,PHP_CLI_SERVER_WORKERS:String(a),APP_CONFIG_CACHE:`bootstrap/cache/${e}/config.php`,APP_ROUTES_CACHE:`bootstrap/cache/${e}/routes.php`,APP_EVENTS_CACHE:`bootstrap/cache/${e}/events.php`},reuseExistingServer:!process.env.CI}}function v(i,e){if(!e)return!1;const t=new URL(e).hostname;return i.hostname===t||i.hostname.endsWith(`.${t}`)}const h="X-Playwright-Test",P=l.test.extend({laravelBaseUrl:[void 0,{option:!0}],laravelSecret:[void 0,{option:!0}],laravelIsAppUrl:[void 0,{option:!0}],laravelTestId:async({},i)=>{await i(crypto.randomUUID())},context:async({context:i,baseURL:e,laravelIsAppUrl:t,laravelTestId:a},r)=>{const s=t??(o=>v(o,e));await i.route(s,o=>o.fallback({headers:{...o.request().headers(),[h]:a}})),await r(i)},laravel:async({laravelBaseUrl:i,laravelSecret:e,laravelTestId:t,baseURL:a,request:r},s)=>{const o=i||a+"/playwright",c=new w(o,r,e,t);await s(c),await c.tearDown()}});class w{constructor(e,t,a=void 0,r=crypto.randomUUID()){this.baseUrl=e,this.request=t,this.secret=a,this.testId=r}async call(e,t={}){const a=this.baseUrl.replace(/\/$/,"")+e,r={Accept:"application/json",[h]:this.testId};this.secret&&(r["X-Playwright-Secret"]=this.secret);const s=await this.request.post(a,{data:t,headers:r});if(s.status()!==200)throw new Error(`
|
|
2
|
+
Failed to call Laravel ${e}.
|
|
3
|
+
Status: ${s.status()}
|
|
4
|
+
Response: ${await s.text()}
|
|
5
|
+
`);return await s.json()}async artisan(e,t=[]){return await this.call("/artisan",{command:e,parameters:t})}async truncate(e=[]){return await this.call("/truncate",{connections:e})}async factory(e,t={},a){return await this.call("/factory",{model:e,count:a,attrs:t})}async query(e,t=[],a={}){const{connection:r=null,unprepared:s=!1}=a;if(s&&t.length>0)throw new Error("Cannot use unprepared with bindings");return await this.call("/query",{query:e,bindings:t,connection:r,unprepared:s})}async select(e,t={},a={}){const{connection:r=null}=a;return await this.call("/select",{query:e,bindings:t,connection:r})}async callFunction(e,t=[]){return await this.call("/function",{function:e,args:t})}async config(e,t){return await this.call("/dynamicConfig",{key:e,value:t})}async travel(e){return await this.call("/travel",{to:e})}async registerBootFunction(e){return await this.call("/registerBootFunction",{function:e})}async tearDown(){return await this.call("/tearDown")}}n.Laravel=w,n.TEST_HEADER=h,n.isAppUrl=v,n.laravelServer=y,n.test=P,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saucebase/laravel-playwright",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "v2.1.0",
|
|
4
4
|
"description": "Blends Laravel and Playwright into seamless E2E tests — factories, queries, artisan commands, and time travel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"homepage": "https://github.com/saucebase-dev/laravel-playwright",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "tsc && vite build"
|
|
16
|
+
"build": "tsc && vite build",
|
|
17
|
+
"test": "node --test 'src/playwright/tests/*.test.ts'"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@types/node": "^25.3.1",
|