@itmetsam/samurai-framework 0.8.0 → 0.10.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 +9 -9
- package/dist/browser/browser-finder.js +2 -2
- package/dist/browser/browser.d.ts +11 -15
- package/dist/browser/browser.js +79 -58
- package/dist/browser/chrome-launch.d.ts +69 -0
- package/dist/browser/chrome-launch.js +170 -0
- package/dist/browser/download-folders.d.ts +27 -0
- package/dist/browser/download-folders.js +97 -0
- package/dist/browser/page-logs.d.ts +2 -0
- package/dist/browser/page-logs.js +25 -5
- package/dist/browser/page.d.ts +18 -0
- package/dist/browser/page.js +99 -2
- package/dist/browser/screenshot.js +16 -2
- package/dist/browser/session-capabilities.d.ts +12 -0
- package/dist/browser/session-capabilities.js +8 -0
- package/dist/browsers/index.d.ts +2 -2
- package/dist/browsers/index.js +2 -2
- package/dist/browsers/overrides.d.ts +2 -0
- package/dist/browsers/overrides.js +2 -0
- package/dist/browsers/pins.d.ts +1 -1
- package/dist/browsers/pins.js +2 -2
- package/dist/browsers/resolve.d.ts +2 -0
- package/dist/browsers/resolve.js +14 -2
- package/dist/browsers/store.d.ts +2 -0
- package/dist/browsers/store.js +8 -0
- package/dist/cli/args.d.ts +7 -2
- package/dist/cli/args.js +7 -1
- package/dist/cli/console-reporter.d.ts +4 -0
- package/dist/cli/console-reporter.js +13 -6
- package/dist/cli/init.js +1 -1
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +7 -0
- package/dist/config/config.js +5 -1
- package/dist/flows/run/handlers/support.d.ts +4 -1
- package/dist/flows/run/handlers/support.js +1 -1
- package/dist/flows/run/project.js +9 -1
- package/dist/flows/run/run.d.ts +5 -0
- package/dist/flows/run/run.js +7 -1
- package/dist/locator/locator.js +21 -2
- package/dist/network/data-collector.d.ts +1 -0
- package/dist/network/data-collector.js +23 -5
- package/dist/network/network-tracker.d.ts +8 -1
- package/dist/network/network-tracker.js +42 -3
- package/dist/network/router.d.ts +5 -0
- package/dist/network/router.js +33 -11
- package/dist/recorder/session.d.ts +3 -0
- package/dist/recorder/session.js +7 -3
- package/dist/recorder/toolbar/controller.d.ts +6 -5
- package/dist/recorder/toolbar/controller.js +39 -7
- package/dist/runner/browsers.d.ts +22 -0
- package/dist/runner/browsers.js +44 -0
- package/dist/runner/groups.d.ts +5 -1
- package/dist/runner/groups.js +9 -2
- package/dist/runner/reporter.d.ts +10 -2
- package/dist/runner/reporter.js +30 -10
- package/dist/runner/run.d.ts +1 -0
- package/dist/runner/run.js +1 -0
- package/dist/runner/test-runner.d.ts +9 -1
- package/dist/runner/test-runner.js +46 -19
- package/dist/script/preload.js +65 -0
- package/dist/transport/bidi-connection.d.ts +19 -0
- package/dist/transport/bidi-connection.js +34 -1
- package/dist/types/browser.d.ts +1 -1
- package/dist/types/config.d.ts +5 -3
- package/dist/types/test.d.ts +8 -0
- package/package.json +3 -2
|
@@ -4,6 +4,8 @@ import path from "path";
|
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
5
|
import { Browser } from "../browser/browser.js";
|
|
6
6
|
import { ensureWarm } from "../browser/warm-up.js";
|
|
7
|
+
import { projectBrowser } from "../browsers/resolve.js";
|
|
8
|
+
import { browsersFor } from "./browsers.js";
|
|
7
9
|
import { loadConfig, projectDir, readConfig } from "../config/config.js";
|
|
8
10
|
import logger from "../logger/index.js";
|
|
9
11
|
import { assertModuleProject } from "./module-type.js";
|
|
@@ -108,34 +110,59 @@ export default class TestRunner {
|
|
|
108
110
|
}
|
|
109
111
|
return all.filter((test) => inGroup(test) && isSelected(test.name, this.options));
|
|
110
112
|
}
|
|
111
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The browsers the run uses: the options' (`--browser`), else the selected group's, else the project's, else
|
|
115
|
+
* Firefox. A group is only looked up when there is something to look up
|
|
116
|
+
*/
|
|
117
|
+
async browsersOfRun() {
|
|
118
|
+
const { browsers: flag, group } = this.options;
|
|
119
|
+
const config = await loadConfig();
|
|
120
|
+
let ofGroup;
|
|
121
|
+
if (group !== undefined && !(flag && flag.length > 0)) {
|
|
122
|
+
const { groups } = await this.resolveAll(await this.registerAll());
|
|
123
|
+
ofGroup = findGroup(groups, group).browsers;
|
|
124
|
+
}
|
|
125
|
+
return browsersFor({ flag, group: ofGroup, project: config.browsers });
|
|
126
|
+
}
|
|
127
|
+
/** Runs the selected tests in every browser of the run, one browser after the other, and returns the summary that was also written to the report */
|
|
112
128
|
async start() {
|
|
113
129
|
const { reportPath, onEvent, signal } = this.options;
|
|
130
|
+
const selected = await this.register();
|
|
131
|
+
const browsers = await this.browsersOfRun();
|
|
114
132
|
const reporter = new TestReporter({
|
|
115
133
|
environment: this.run.settings.environment,
|
|
134
|
+
browsers,
|
|
116
135
|
...(reportPath !== undefined && { output: reportPath }),
|
|
117
136
|
...(onEvent && { onEvent }),
|
|
118
137
|
});
|
|
119
|
-
const selected = await this.register();
|
|
120
138
|
this.srcDir = realDirs((await loadConfig()).srcDir).srcDir;
|
|
121
|
-
// A missing browser is one clear error before the run, not a failure per test
|
|
122
|
-
// first start happens here, outside any test's timeout
|
|
123
|
-
if (selected.length > 0)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
// A missing browser is one clear error before the run, not a failure per test, and before any browser has run a
|
|
140
|
+
// test; a fresh build's slow first start happens here too, outside any test's timeout
|
|
141
|
+
if (selected.length > 0) {
|
|
142
|
+
for (const browser of browsers)
|
|
143
|
+
await projectBrowser(browser);
|
|
144
|
+
for (const browser of browsers)
|
|
145
|
+
await ensureWarm(browser, { port: this.options.port ?? 9223 }, signal);
|
|
146
|
+
}
|
|
147
|
+
reporter.onStart(selected.length * browsers.length);
|
|
148
|
+
for (const browser of browsers) {
|
|
149
|
+
for (const test of selected) {
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
reporter.onTestStart(test, browser);
|
|
152
|
+
reporter.onTestEnd(test, browser, {
|
|
153
|
+
message: "Run aborted",
|
|
154
|
+
type: "error",
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
await this.executeTestCase(test, browser, reporter);
|
|
131
159
|
}
|
|
132
|
-
await this.executeTestCase(test, reporter);
|
|
133
160
|
}
|
|
134
161
|
return reporter.onEnd();
|
|
135
162
|
}
|
|
136
|
-
async executeTestCase(test, reporter) {
|
|
137
|
-
logger.verbose("Starting test: %s", test.name);
|
|
138
|
-
reporter.onTestStart(test);
|
|
163
|
+
async executeTestCase(test, selectedBrowser, reporter) {
|
|
164
|
+
logger.verbose("Starting test: %s in %s", test.name, selectedBrowser);
|
|
165
|
+
reporter.onTestStart(test, selectedBrowser);
|
|
139
166
|
// A test ends once, whichever of finishing, failing, timing out, an abort or a closed browser comes first
|
|
140
167
|
let ended = false;
|
|
141
168
|
let evidence;
|
|
@@ -143,7 +170,7 @@ export default class TestRunner {
|
|
|
143
170
|
if (ended)
|
|
144
171
|
return;
|
|
145
172
|
ended = true;
|
|
146
|
-
reporter.onTestEnd(test, error, {
|
|
173
|
+
reporter.onTestEnd(test, selectedBrowser, error, {
|
|
147
174
|
...logs,
|
|
148
175
|
screenshots: evidence?.screenshots,
|
|
149
176
|
});
|
|
@@ -151,7 +178,6 @@ export default class TestRunner {
|
|
|
151
178
|
let browserClosed = false;
|
|
152
179
|
/** Set once the runner itself closes the browser */
|
|
153
180
|
let closing = false;
|
|
154
|
-
const selectedBrowser = (await readConfig("browser")) ?? "firefox";
|
|
155
181
|
const { settings, secrets } = this.run;
|
|
156
182
|
const timeout = settings.timeout;
|
|
157
183
|
const { reportPath } = this.options;
|
|
@@ -205,11 +231,12 @@ export default class TestRunner {
|
|
|
205
231
|
resolve();
|
|
206
232
|
});
|
|
207
233
|
page = launched.page;
|
|
208
|
-
evidence = new Evidence(resultDir, testDirOf(test, this.srcDir));
|
|
234
|
+
evidence = new Evidence(resultDir, `${selectedBrowser}/${testDirOf(test, this.srcDir)}`);
|
|
209
235
|
launched.page.setEvidence(evidence);
|
|
210
236
|
const fixtures = {
|
|
211
237
|
page: launched.page,
|
|
212
238
|
browser: launched.browser,
|
|
239
|
+
browserName: selectedBrowser,
|
|
213
240
|
env: createEnvFixture(settings.environment, settings.variables),
|
|
214
241
|
secrets: createSecretsFixture(settings.environment, secrets),
|
|
215
242
|
};
|
package/dist/script/preload.js
CHANGED
|
@@ -16,11 +16,24 @@ export async function addPreload(connector, options) {
|
|
|
16
16
|
arguments: options.arguments.map((arg) => toLocalValue(arg)),
|
|
17
17
|
}),
|
|
18
18
|
});
|
|
19
|
+
let stopRerun;
|
|
20
|
+
if (needsRerun(connector, options)) {
|
|
21
|
+
try {
|
|
22
|
+
stopRerun = await rerunInNewRealms(connector, options);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
await connector
|
|
26
|
+
.send("script.removePreloadScript", { script })
|
|
27
|
+
.catch(() => undefined);
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
let removed = false;
|
|
20
32
|
const dispose = async () => {
|
|
21
33
|
if (removed)
|
|
22
34
|
return;
|
|
23
35
|
removed = true;
|
|
36
|
+
await stopRerun?.();
|
|
24
37
|
try {
|
|
25
38
|
await connector.send("script.removePreloadScript", { script });
|
|
26
39
|
}
|
|
@@ -42,6 +55,58 @@ export async function addPreload(connector, options) {
|
|
|
42
55
|
}
|
|
43
56
|
return { id: script, dispose };
|
|
44
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Chrome drops the messages of a channel that a preload script in a sandbox was given, while a channel handed to a
|
|
60
|
+
* call into the same sandbox works. So there the script is also run, with its channels, in every new sandbox realm.
|
|
61
|
+
*/
|
|
62
|
+
function needsRerun(connector, options) {
|
|
63
|
+
return (connector.browserName === "chrome" &&
|
|
64
|
+
options.sandbox !== undefined &&
|
|
65
|
+
(options.arguments?.length ?? 0) > 0);
|
|
66
|
+
}
|
|
67
|
+
async function rerunInNewRealms(connector, options) {
|
|
68
|
+
const { contexts, sandbox, source } = options;
|
|
69
|
+
const inScope = async (context) => {
|
|
70
|
+
if (!contexts)
|
|
71
|
+
return true;
|
|
72
|
+
if (contexts.includes(context))
|
|
73
|
+
return true;
|
|
74
|
+
const trees = await Promise.all(contexts.map((root) => connector.send("browsingContext.getTree", { root })));
|
|
75
|
+
return trees.some((tree) => tree.contexts.flatMap(flatten).includes(context));
|
|
76
|
+
};
|
|
77
|
+
const onRealm = (realm) => {
|
|
78
|
+
if (realm.type !== "window" || realm.sandbox !== sandbox || !realm.context)
|
|
79
|
+
return;
|
|
80
|
+
const context = realm.context;
|
|
81
|
+
void (async () => {
|
|
82
|
+
try {
|
|
83
|
+
if (!(await inScope(context)))
|
|
84
|
+
return;
|
|
85
|
+
await callFunction(connector, context, source, options.arguments ?? [], {
|
|
86
|
+
awaitPromise: false,
|
|
87
|
+
...(sandbox !== undefined && { sandbox }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// The document went away again, or the script threw: the registration covers what it can
|
|
92
|
+
logger.debug("Could not run a preload script in a new realm", { err });
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
};
|
|
96
|
+
connector.onEvent("script.realmCreated", onRealm);
|
|
97
|
+
let subscription;
|
|
98
|
+
try {
|
|
99
|
+
subscription = await connector.subscribe(["script.realmCreated"]);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
connector.offEvent("script.realmCreated", onRealm);
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
return async () => {
|
|
106
|
+
connector.offEvent("script.realmCreated", onRealm);
|
|
107
|
+
await subscription.unsubscribe().catch(() => undefined);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
45
110
|
/** Runs `source` once in every loaded context (iframes included) under `contexts` */
|
|
46
111
|
export async function runInLoadedContexts(connector, { source, contexts, sandbox, onRunError = "throw", arguments: args = [], }) {
|
|
47
112
|
const trees = contexts
|
|
@@ -25,12 +25,18 @@ export declare class BiDiConnector {
|
|
|
25
25
|
private currentId;
|
|
26
26
|
/** Set once the socket closed or failed: nothing sent after that gets an answer */
|
|
27
27
|
private closed;
|
|
28
|
+
/** Which browser is on the other end, when the launcher says; code that must treat browsers apart asks this */
|
|
29
|
+
browserName?: "firefox" | "chrome";
|
|
28
30
|
private webSocket;
|
|
29
31
|
private eventEmitter;
|
|
30
32
|
private commandTimeout;
|
|
31
33
|
private subscriptions;
|
|
32
34
|
constructor(ws: WebSocket, options?: ConnectorOptions);
|
|
33
35
|
static connect(url: string, options?: ConnectorOptions): Promise<BiDiConnector>;
|
|
36
|
+
/** Calls `listener` once when the socket closes or fails (not when `kill()` ends it) */
|
|
37
|
+
onClosed(listener: () => void): void;
|
|
38
|
+
private closeListeners;
|
|
39
|
+
private notifyClosed;
|
|
34
40
|
private onWebsocketClose;
|
|
35
41
|
private onWebsocketError;
|
|
36
42
|
private get isClosed();
|
|
@@ -59,3 +65,16 @@ export declare class BiDiConnector {
|
|
|
59
65
|
private messageListener;
|
|
60
66
|
kill(): void;
|
|
61
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The message as the debug log gets it: a `script.message` on a private channel is logged by channel and
|
|
70
|
+
* action only, so a value the tester types or a secret added from the recorder toolbar never reaches the log
|
|
71
|
+
* (an added secret is only registered for masking after it is saved).
|
|
72
|
+
*/
|
|
73
|
+
export declare function forLog(message: {
|
|
74
|
+
type?: string;
|
|
75
|
+
method?: string;
|
|
76
|
+
params?: {
|
|
77
|
+
channel?: unknown;
|
|
78
|
+
data?: unknown;
|
|
79
|
+
};
|
|
80
|
+
}): unknown;
|
|
@@ -22,12 +22,15 @@ export class BiDiConnector {
|
|
|
22
22
|
this.currentId = 0;
|
|
23
23
|
/** Set once the socket closed or failed: nothing sent after that gets an answer */
|
|
24
24
|
this.closed = false;
|
|
25
|
+
this.closeListeners = new Set();
|
|
25
26
|
this.onWebsocketClose = () => {
|
|
26
27
|
this.closed = true;
|
|
28
|
+
this.notifyClosed();
|
|
27
29
|
this.rejectAll((pending) => new ConnectionClosedError(`Websocket closed${pendingSuffix(pending)}`));
|
|
28
30
|
};
|
|
29
31
|
this.onWebsocketError = (ev) => {
|
|
30
32
|
this.closed = true;
|
|
33
|
+
this.notifyClosed();
|
|
31
34
|
this.rejectAll((pending) => new ConnectionClosedError(`Websocket error${pendingSuffix(pending)}`, {
|
|
32
35
|
cause: ev,
|
|
33
36
|
}));
|
|
@@ -84,6 +87,16 @@ export class BiDiConnector {
|
|
|
84
87
|
});
|
|
85
88
|
});
|
|
86
89
|
}
|
|
90
|
+
/** Calls `listener` once when the socket closes or fails (not when `kill()` ends it) */
|
|
91
|
+
onClosed(listener) {
|
|
92
|
+
this.closeListeners.add(listener);
|
|
93
|
+
}
|
|
94
|
+
notifyClosed() {
|
|
95
|
+
const listeners = [...this.closeListeners];
|
|
96
|
+
this.closeListeners.clear();
|
|
97
|
+
for (const listener of listeners)
|
|
98
|
+
listener();
|
|
99
|
+
}
|
|
87
100
|
get isClosed() {
|
|
88
101
|
// A socket that is closing or closed (readyState 2 or 3) has already lost the browser
|
|
89
102
|
const state = this.webSocket.readyState;
|
|
@@ -203,7 +216,27 @@ const PRIVATE_CHANNEL = /^samurai-(recorder|toolbar)-/;
|
|
|
203
216
|
* action only, so a value the tester types or a secret added from the recorder toolbar never reaches the log
|
|
204
217
|
* (an added secret is only registered for masking after it is saved).
|
|
205
218
|
*/
|
|
206
|
-
function forLog(message) {
|
|
219
|
+
export function forLog(message) {
|
|
220
|
+
return shortened(redacted(message));
|
|
221
|
+
}
|
|
222
|
+
/** A string longer than this is cut in the log: a file's bytes travel in 1 MB chunks, 50 MB makes 70 MB of debug log */
|
|
223
|
+
const LOG_STRING_LIMIT = 1000;
|
|
224
|
+
/** A copy of `value` with every long string cut to its start and its length; short values come back as they are */
|
|
225
|
+
function shortened(value, depth = 0) {
|
|
226
|
+
if (typeof value === "string")
|
|
227
|
+
return value.length > LOG_STRING_LIMIT
|
|
228
|
+
? `${value.slice(0, 200)}… (${value.length} characters)`
|
|
229
|
+
: value;
|
|
230
|
+
if (value === null || typeof value !== "object" || depth > 30)
|
|
231
|
+
return value;
|
|
232
|
+
if (Array.isArray(value))
|
|
233
|
+
return value.map((item) => shortened(item, depth + 1));
|
|
234
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
235
|
+
key,
|
|
236
|
+
shortened(item, depth + 1),
|
|
237
|
+
]));
|
|
238
|
+
}
|
|
239
|
+
function redacted(message) {
|
|
207
240
|
const channel = message.params?.channel;
|
|
208
241
|
if (message.type !== "event" ||
|
|
209
242
|
message.method !== "script.message" ||
|
package/dist/types/browser.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type SupportedBrowser = "firefox";
|
|
1
|
+
export type SupportedBrowser = "firefox" | "chrome";
|
package/dist/types/config.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface SamuraiGroup {
|
|
|
24
24
|
* Overrides 'src' and 'testMatch' options.
|
|
25
25
|
*/
|
|
26
26
|
tests?: SamuraiTest[];
|
|
27
|
+
/** The browsers this group's tests run in, instead of the project's `browsers` */
|
|
28
|
+
browsers?: SupportedBrowser[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface SamuraiEnvironment {
|
|
@@ -45,10 +47,10 @@ export interface SamuraiTestConfig {
|
|
|
45
47
|
*/
|
|
46
48
|
srcDir?: string;
|
|
47
49
|
/**
|
|
48
|
-
* The
|
|
49
|
-
* @default "firefox"
|
|
50
|
+
* The browsers the tests run in, one after the other: every selected test runs once in each. A group can name its own.
|
|
51
|
+
* @default ["firefox"]
|
|
50
52
|
*/
|
|
51
|
-
|
|
53
|
+
browsers?: SupportedBrowser[];
|
|
52
54
|
/**
|
|
53
55
|
* Browser binaries to launch instead of the builds SAMURAI downloads, relative to the project.
|
|
54
56
|
* `SAMURAI_FIREFOX_PATH` / `SAMURAI_CHROME_PATH` win over these.
|
package/dist/types/test.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Browser } from "../browser/browser.ts";
|
|
2
2
|
import type Page from "../browser/page.ts";
|
|
3
|
+
import type { SupportedBrowser } from "./browser.ts";
|
|
3
4
|
|
|
4
5
|
/** What a test function receives */
|
|
5
6
|
export interface TestFixtures {
|
|
6
7
|
page: Page;
|
|
7
8
|
browser: Browser;
|
|
9
|
+
/** The browser this run of the test is in */
|
|
10
|
+
browserName: SupportedBrowser;
|
|
8
11
|
/** The chosen environment's variables; reading an unknown name throws */
|
|
9
12
|
env: Readonly<Record<string, string | number | boolean>>;
|
|
10
13
|
/** Secrets by name (SAMURAI_SECRET_<NAME>); reading an unset name throws */
|
|
@@ -66,12 +69,15 @@ export interface TestLogEntry {
|
|
|
66
69
|
interface TestResultIdentity {
|
|
67
70
|
name: string;
|
|
68
71
|
file: string;
|
|
72
|
+
/** The browser the test ran in */
|
|
73
|
+
browser: SupportedBrowser;
|
|
69
74
|
startTime: number;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
interface TestResultBase {
|
|
73
78
|
name: string;
|
|
74
79
|
file: string;
|
|
80
|
+
browser: SupportedBrowser;
|
|
75
81
|
startTime: number;
|
|
76
82
|
status: "started";
|
|
77
83
|
}
|
|
@@ -102,4 +108,6 @@ interface TestSummaryBase {
|
|
|
102
108
|
tests: TestResult[];
|
|
103
109
|
/** The environment the run used */
|
|
104
110
|
environment: string;
|
|
111
|
+
/** The browsers the run used, in the order they ran */
|
|
112
|
+
browsers: SupportedBrowser[];
|
|
105
113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"exports": {
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"dev": "tsx src/index.ts",
|
|
55
55
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-types.mjs",
|
|
56
56
|
"test": "tsx --test --test-timeout=10000 \"src/**/*.test.ts\"",
|
|
57
|
-
"test:browser": "tsx --test --test-concurrency=1 \"src/**/*.browser-test.ts\"",
|
|
57
|
+
"test:browser": "tsx --test --test-concurrency=1 --test-timeout=60000 \"src/**/*.browser-test.ts\"",
|
|
58
|
+
"test:browser:chrome": "SAMURAI_TEST_BROWSER=chrome npm run test:browser",
|
|
58
59
|
"test:cli": "tsx --test --test-concurrency=1 \"src/**/*.cli-test.ts\""
|
|
59
60
|
},
|
|
60
61
|
"keywords": [
|