@saucebase/laravel-playwright 1.0.0 → 2.0.1

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 CHANGED
@@ -45,9 +45,10 @@ return [
45
45
 
46
46
  /**
47
47
  * The environments in which the testing endpoints are enabled.
48
- * CAUTION: Enabling testing endpoints in production is a critical security issue.
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' => ['local', 'testing'],
51
+ 'environments' => ['e2e', 'testing'],
51
52
 
52
53
  /**
53
54
  * Optional secret token to authenticate Playwright requests.
@@ -88,6 +89,45 @@ 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`.** Copy `.env` and give it its own database and port:
97
+
98
+ ```dotenv
99
+ APP_ENV=e2e
100
+ APP_URL=http://localhost:8100
101
+ DB_CONNECTION=sqlite
102
+ DB_DATABASE=database/e2e.sqlite
103
+ SESSION_DRIVER=file
104
+ CACHE_STORE=file
105
+ QUEUE_CONNECTION=sync
106
+ MAIL_MAILER=log
107
+ ```
108
+
109
+ Keep `.env.e2e` out of git like `.env`.
110
+
111
+ **2. Let Playwright start the server:**
112
+
113
+ ```ts
114
+ import { defineConfig } from '@playwright/test';
115
+ import { laravelServer, type LaravelOptions } from '@saucebase/laravel-playwright';
116
+
117
+ export default defineConfig<LaravelOptions>({
118
+ use: { baseURL: 'http://localhost:8100' },
119
+ webServer: laravelServer(), // migrate, then php artisan serve --env=e2e on APP_URL's port
120
+ });
121
+ ```
122
+
123
+ `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:
124
+
125
+ - `.env.e2e` is missing, because Laravel would then fall back to `.env`.
126
+ - `APP_URL` is missing or not `http:`. `artisan serve` has no TLS.
127
+ - 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.
128
+
129
+ **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`.
130
+
91
131
  ## 🔒 Security
92
132
 
93
133
  The package supports an optional shared secret to prevent unauthorized access to the testing endpoints.
@@ -205,7 +245,13 @@ test('example', async ({ laravel }) => {
205
245
 
206
246
  ## 🔄 Dynamic Configuration
207
247
 
208
- You can update Laravel config for **ALL** subsequent requests until the test ends.
248
+ You can update Laravel config for every subsequent request **of the same test**, until the test ends.
249
+
250
+ 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.
251
+
252
+ - 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.
253
+ - 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.
254
+ - A context you create yourself (`browser.newContext()`) doesn't get the header.
209
255
 
210
256
  ```ts
211
257
  import { test } from '@saucebase/laravel-playwright';
@@ -283,7 +329,19 @@ test('checkout with fake payment gateway', async ({ laravel, page }) => {
283
329
  > **Why not use `callFunction` instead?**
284
330
  > `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
331
 
286
- > **Note:** The helper class only needs to exist in your `local` and `testing` environments. You can guard it with an environment check or keep it out of production deployments entirely.
332
+ > **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.
333
+
334
+ ## 🤖 Laravel Boost
335
+
336
+ 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`.
337
+
338
+ ## ⬆️ Upgrading from v1
339
+
340
+ - **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.
341
+ - **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.
342
+ - `DynamicConfig::set()`, `get()`, `getAll()`, `load()` and `delete()` now take the test id as their first argument.
343
+ - **Artisan commands run from the application root**, like the CLI, rather than the web server's `public/`.
344
+ - Requires Laravel 12 or 13.
287
345
 
288
346
  ## 🙏 Credits
289
347
 
@@ -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
- constructor(baseUrl: string, request: APIRequestContext, secret?: string | undefined);
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 i } from "@playwright/test";
2
- const u = i.extend({
1
+ import { test as h } from "@playwright/test";
2
+ import { existsSync as p, readFileSync as u } from "node:fs";
3
+ import { join as v } 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 = v(r, `.env.${t}`);
7
+ if (!p(o))
8
+ throw new Error(`laravelServer: create .env.${t} in ${r}, with its own database, before running the suite.`);
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(u(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
- laravel: async ({ laravelBaseUrl: s, laravelSecret: t, baseURL: a, request: e }, n) => {
6
- const r = s || a + "/playwright", c = new l(r, e, t);
7
- await n(c), await c.tearDown();
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 l {
11
- constructor(t, a, e = void 0) {
12
- this.baseUrl = t, this.request = a, this.secret = e;
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, a = {}) {
15
- const e = this.baseUrl.replace(/\/$/, "") + t, n = { Accept: "application/json" };
16
- this.secret && (n["X-Playwright-Secret"] = this.secret);
17
- const r = await this.request.post(e, { data: a, headers: n });
18
- if (r.status() !== 200)
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: ${r.status()}
22
- Response: ${await r.text()}
81
+ Status: ${n.status()}
82
+ Response: ${await n.text()}
23
83
  `);
24
- return await r.json();
84
+ return await n.json();
25
85
  }
26
- async artisan(t, a = []) {
27
- return await this.call("/artisan", { command: t, parameters: a });
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, a = {}, e) {
33
- return await this.call("/factory", { model: t, count: e, attrs: a });
92
+ async factory(t, e = {}, a) {
93
+ return await this.call("/factory", { model: t, count: a, attrs: e });
34
94
  }
35
- async query(t, a = [], e = {}) {
36
- const { connection: n = null, unprepared: r = !1 } = e;
37
- if (r && a.length > 0)
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: a,
42
- connection: n,
43
- unprepared: r
101
+ bindings: e,
102
+ connection: r,
103
+ unprepared: n
44
104
  });
45
105
  }
46
- async select(t, a = {}, e = {}) {
47
- const { connection: n = null } = e;
48
- return await this.call("/select", { query: t, bindings: a, connection: n });
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, a = []) {
51
- return await this.call("/function", { function: t, args: a });
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, a) {
57
- return await this.call("/dynamicConfig", { key: t, value: a });
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
- l as Laravel,
75
- u as test
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(r,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("@playwright/test")):typeof define=="function"&&define.amd?define(["exports","@playwright/test"],i):(r=typeof globalThis<"u"?globalThis:r||self,i(r.LaravelPlaywright={},r.PlaywrightTest))})(this,(function(r,i){"use strict";const u=i.test.extend({laravelBaseUrl:[void 0,{option:!0}],laravelSecret:[void 0,{option:!0}],laravel:async({laravelBaseUrl:l,laravelSecret:t,baseURL:e,request:a},s)=>{const n=l||e+"/playwright",o=new c(n,a,t);await s(o),await o.tearDown()}});class c{constructor(t,e,a=void 0){this.baseUrl=t,this.request=e,this.secret=a}async call(t,e={}){const a=this.baseUrl.replace(/\/$/,"")+t,s={Accept:"application/json"};this.secret&&(s["X-Playwright-Secret"]=this.secret);const n=await this.request.post(a,{data:e,headers:s});if(n.status()!==200)throw new Error(`
2
- Failed to call Laravel ${t}.
3
- Status: ${n.status()}
4
- Response: ${await n.text()}
5
- `);return await n.json()}async artisan(t,e=[]){return await this.call("/artisan",{command:t,parameters:e})}async truncate(t=[]){return await this.call("/truncate",{connections:t})}async factory(t,e={},a){return await this.call("/factory",{model:t,count:a,attrs:e})}async query(t,e=[],a={}){const{connection:s=null,unprepared:n=!1}=a;if(n&&e.length>0)throw new Error("Cannot use unprepared with bindings");return await this.call("/query",{query:t,bindings:e,connection:s,unprepared:n})}async select(t,e={},a={}){const{connection:s=null}=a;return await this.call("/select",{query:t,bindings:e,connection:s})}async callFunction(t,e=[]){return await this.call("/function",{function:t,args:e})}async config(t,e){return await this.call("/dynamicConfig",{key:t,value:e})}async travel(t){return await this.call("/travel",{to:t})}async registerBootFunction(t){return await this.call("/registerBootFunction",{function:t})}async tearDown(){return await this.call("/tearDown")}}r.Laravel=c,r.test=u,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})}));
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(o={}){const{env:e="e2e",host:t="127.0.0.1",workers:a=4,cwd:r=process.cwd(),migrate:s=!0}=o,i=d.join(r,`.env.${e}`);if(!p.existsSync(i))throw new Error(`laravelServer: create .env.${e} in ${r}, with its own database, before running the suite.`);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(i,"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(o,e){if(!e)return!1;const t=new URL(e).hostname;return o.hostname===t||o.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({},o)=>{await o(crypto.randomUUID())},context:async({context:o,baseURL:e,laravelIsAppUrl:t,laravelTestId:a},r)=>{const s=t??(i=>v(i,e));await o.route(s,i=>i.fallback({headers:{...i.request().headers(),[h]:a}})),await r(o)},laravel:async({laravelBaseUrl:o,laravelSecret:e,laravelTestId:t,baseURL:a,request:r},s)=>{const i=o||a+"/playwright",c=new w(i,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": "v1.0.0",
3
+ "version": "v2.0.1",
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",