@untestutils/utils 0.5.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/LICENSE +21 -0
- package/dist/browser.d.ts +40 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.mjs +71 -0
- package/dist/cookies.d.mts +16 -0
- package/dist/cookies.d.ts +17 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.mjs +20 -0
- package/dist/headers.d.mts +4 -0
- package/dist/headers.d.ts +5 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.mjs +11 -0
- package/dist/http.d.mts +12 -0
- package/dist/http.d.ts +13 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.mjs +38 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7 -0
- package/dist/poll.d.mts +9 -0
- package/dist/poll.d.ts +10 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.mjs +19 -0
- package/dist/seo.d.mts +27 -0
- package/dist/seo.d.ts +28 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.mjs +70 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 s00d
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Page, Response } from 'playwright-core';
|
|
2
|
+
/**
|
|
3
|
+
* Proxy browser requests from `emulatedOrigin` to `realBaseURL`, forwarding
|
|
4
|
+
* `X-Forwarded-Host` / `X-Forwarded-Proto` so SSR sees the public domain.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emulateDomain(page: Page, emulatedOrigin: string, realBaseURL: string): Promise<void>;
|
|
7
|
+
export type TrackedResponse = {
|
|
8
|
+
url: string;
|
|
9
|
+
status: number;
|
|
10
|
+
response: Response;
|
|
11
|
+
};
|
|
12
|
+
/** Collect HTTP responses for redirect assertions. Returns live array + disposer. */
|
|
13
|
+
export declare function trackResponses(page: Page): {
|
|
14
|
+
responses: TrackedResponse[];
|
|
15
|
+
stop: () => void;
|
|
16
|
+
findByPath: (pathname: string) => TrackedResponse | undefined;
|
|
17
|
+
};
|
|
18
|
+
export type ConsoleMatch = {
|
|
19
|
+
type: string;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Collect console messages matching a predicate (hydration warnings, etc.).
|
|
24
|
+
* Call `stop()` when done; `messages` stays filled.
|
|
25
|
+
*/
|
|
26
|
+
export declare function collectConsole(page: Page, predicate?: (msg: {
|
|
27
|
+
type: () => string;
|
|
28
|
+
text: () => string;
|
|
29
|
+
}) => boolean): {
|
|
30
|
+
messages: ConsoleMatch[];
|
|
31
|
+
stop: () => void;
|
|
32
|
+
};
|
|
33
|
+
/** Collect hydration / mismatch console errors & warnings. */
|
|
34
|
+
export declare function collectHydrationIssues(page: Page): {
|
|
35
|
+
messages: ConsoleMatch[];
|
|
36
|
+
stop: () => void;
|
|
37
|
+
};
|
|
38
|
+
/** Apply `Accept-Language` for subsequent navigations on this page. */
|
|
39
|
+
export declare function setAcceptLanguage(page: Page, value: string): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGtD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAElF,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG;IAC1C,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;CAC/D,CAkBA;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,IAAI,EACV,SAAS,GAAE,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,MAAM,CAAA;CAAE,KAAK,OAAoB,GACnF;IAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAUhD;AAED,8DAA8D;AAC9D,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI;cAblC,YAAY,EAAE;UAAQ,MAAM,IAAI;EAmB9C;AAED,uEAAuE;AACvE,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhF"}
|
package/dist/browser.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { forwardedHostHeaders } from "./headers.mjs";
|
|
2
|
+
|
|
3
|
+
export async function emulateDomain(page, emulatedOrigin, realBaseURL) {
|
|
4
|
+
const realBase = realBaseURL.replace(/\/$/, "");
|
|
5
|
+
const { hostname: fwdHost } = new URL(emulatedOrigin);
|
|
6
|
+
await page.route(`${emulatedOrigin}/**`, async (route) => {
|
|
7
|
+
const url = new URL(route.request().url());
|
|
8
|
+
try {
|
|
9
|
+
const response = await route.fetch({
|
|
10
|
+
url: `${realBase}${url.pathname}${url.search}`,
|
|
11
|
+
headers: {
|
|
12
|
+
...route.request().headers(),
|
|
13
|
+
...forwardedHostHeaders(fwdHost, url.protocol.replace(":", ""))
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
await route.fulfill({ response });
|
|
17
|
+
} catch {
|
|
18
|
+
await route.abort();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function trackResponses(page) {
|
|
24
|
+
const responses = [];
|
|
25
|
+
const onResponse = (response) => {
|
|
26
|
+
responses.push({
|
|
27
|
+
url: response.url(),
|
|
28
|
+
status: response.status(),
|
|
29
|
+
response
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
page.on("response", onResponse);
|
|
33
|
+
return {
|
|
34
|
+
responses,
|
|
35
|
+
stop: () => page.off("response", onResponse),
|
|
36
|
+
findByPath: (pathname) => responses.find((r) => {
|
|
37
|
+
try {
|
|
38
|
+
return new URL(r.url).pathname === pathname;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function collectConsole(page, predicate = () => true) {
|
|
47
|
+
const messages = [];
|
|
48
|
+
const onConsole = (msg) => {
|
|
49
|
+
if (predicate(msg)) messages.push({
|
|
50
|
+
type: msg.type(),
|
|
51
|
+
text: msg.text()
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
page.on("console", onConsole);
|
|
55
|
+
return {
|
|
56
|
+
messages,
|
|
57
|
+
stop: () => page.off("console", onConsole)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function collectHydrationIssues(page) {
|
|
62
|
+
return collectConsole(page, (msg) => {
|
|
63
|
+
if (msg.type() !== "error" && msg.type() !== "warning") return false;
|
|
64
|
+
const text = msg.text().toLowerCase();
|
|
65
|
+
return text.includes("hydration") || text.includes("mismatch");
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function setAcceptLanguage(page, value) {
|
|
70
|
+
await page.setExtraHTTPHeaders({ "Accept-Language": value });
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Page } from "playwright-core";
|
|
2
|
+
export type CookiePage = Pick<Page, "context">;
|
|
3
|
+
export type SetCookieOptions = {
|
|
4
|
+
/** Cookie name (default `user-locale`) */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Clear all cookies before setting */
|
|
7
|
+
clear?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Set a locale cookie on the browser context (common i18n e2e pattern).
|
|
11
|
+
* `baseURL` must be an absolute origin (harness `baseURL` fixture).
|
|
12
|
+
*/
|
|
13
|
+
export declare function setLocaleCookie(page: CookiePage, baseURL: string, locale: string, options?: SetCookieOptions): Promise<void>;
|
|
14
|
+
/** Read a named cookie value from the context (or `undefined`). */
|
|
15
|
+
export declare function getLocaleCookie(page: CookiePage, name?: string): Promise<string | undefined>;
|
|
16
|
+
export declare function clearCookies(page: CookiePage): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
export type CookiePage = Pick<Page, 'context'>;
|
|
3
|
+
export type SetCookieOptions = {
|
|
4
|
+
/** Cookie name (default `user-locale`) */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Clear all cookies before setting */
|
|
7
|
+
clear?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Set a locale cookie on the browser context (common i18n e2e pattern).
|
|
11
|
+
* `baseURL` must be an absolute origin (harness `baseURL` fixture).
|
|
12
|
+
*/
|
|
13
|
+
export declare function setLocaleCookie(page: CookiePage, baseURL: string, locale: string, options?: SetCookieOptions): Promise<void>;
|
|
14
|
+
/** Read a named cookie value from the context (or `undefined`). */
|
|
15
|
+
export declare function getLocaleCookie(page: CookiePage, name?: string): Promise<string | undefined>;
|
|
16
|
+
export declare function clearCookies(page: CookiePage): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AAE/C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;GAGG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,mEAAmE;AACnE,wBAAsB,eAAe,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,GAAE,MAAsB,GAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAG7B;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE"}
|
package/dist/cookies.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
export async function setLocaleCookie(page, baseURL, locale, options = {}) {
|
|
3
|
+
const name = options.name ?? "user-locale";
|
|
4
|
+
if (options.clear !== false) {
|
|
5
|
+
await page.context().clearCookies();
|
|
6
|
+
}
|
|
7
|
+
await page.context().addCookies([{
|
|
8
|
+
name,
|
|
9
|
+
value: locale,
|
|
10
|
+
url: baseURL
|
|
11
|
+
}]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function getLocaleCookie(page, name = "user-locale") {
|
|
15
|
+
const cookies = await page.context().cookies();
|
|
16
|
+
return cookies.find((c) => c.name === name)?.value;
|
|
17
|
+
}
|
|
18
|
+
export async function clearCookies(page) {
|
|
19
|
+
await page.context().clearCookies();
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Build an `Accept-Language` header map for Playwright `request` / `setExtraHTTPHeaders`. */
|
|
2
|
+
export declare function acceptLanguageHeaders(value: string): Record<string, string>;
|
|
3
|
+
/** Headers for SSR that should resolve the public host (domain strategies / SEO). */
|
|
4
|
+
export declare function forwardedHostHeaders(host: string, proto?: string): Record<string, string>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Build an `Accept-Language` header map for Playwright `request` / `setExtraHTTPHeaders`. */
|
|
2
|
+
export declare function acceptLanguageHeaders(value: string): Record<string, string>;
|
|
3
|
+
/** Headers for SSR that should resolve the public host (domain strategies / SEO). */
|
|
4
|
+
export declare function forwardedHostHeaders(host: string, proto?: string): Record<string, string>;
|
|
5
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE3E;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKjG"}
|
package/dist/headers.mjs
ADDED
package/dist/http.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type PollUntilOptions } from "./poll.mjs";
|
|
2
|
+
export type WaitForUrlOptions = PollUntilOptions & {
|
|
3
|
+
/** Expected HTTP status (default: any 2xx) */
|
|
4
|
+
status?: number | ((status: number) => boolean);
|
|
5
|
+
init?: RequestInit;
|
|
6
|
+
};
|
|
7
|
+
/** Poll until `url` responds OK (or custom status). */
|
|
8
|
+
export declare function waitForUrlOk(url: string, options?: WaitForUrlOptions): Promise<Response>;
|
|
9
|
+
/** Poll until response body includes `needle`. */
|
|
10
|
+
export declare function waitForUrlIncludes(url: string, needle: string, options?: PollUntilOptions & {
|
|
11
|
+
init?: RequestInit;
|
|
12
|
+
}): Promise<string>;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PollUntilOptions } from './poll';
|
|
2
|
+
export type WaitForUrlOptions = PollUntilOptions & {
|
|
3
|
+
/** Expected HTTP status (default: any 2xx) */
|
|
4
|
+
status?: number | ((status: number) => boolean);
|
|
5
|
+
init?: RequestInit;
|
|
6
|
+
};
|
|
7
|
+
/** Poll until `url` responds OK (or custom status). */
|
|
8
|
+
export declare function waitForUrlOk(url: string, options?: WaitForUrlOptions): Promise<Response>;
|
|
9
|
+
/** Poll until response body includes `needle`. */
|
|
10
|
+
export declare function waitForUrlIncludes(url: string, needle: string, options?: PollUntilOptions & {
|
|
11
|
+
init?: RequestInit;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;IAChD,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB,CAAC;AAEF,uDAAuD;AACvD,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAoBnB;AAED,kDAAkD;AAClD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,WAAW,CAAA;CAAO,GACtD,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
|
package/dist/http.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { pollUntil } from "./poll.mjs";
|
|
2
|
+
|
|
3
|
+
export async function waitForUrlOk(url, options = {}) {
|
|
4
|
+
let last;
|
|
5
|
+
await pollUntil(async () => {
|
|
6
|
+
try {
|
|
7
|
+
last = await fetch(url, options.init);
|
|
8
|
+
if (typeof options.status === "function") return options.status(last.status);
|
|
9
|
+
if (typeof options.status === "number") return last.status === options.status;
|
|
10
|
+
return last.ok;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}, {
|
|
15
|
+
intervalMs: options.intervalMs,
|
|
16
|
+
timeoutMs: options.timeoutMs,
|
|
17
|
+
message: options.message ?? `Timed out waiting for OK response from ${url}`
|
|
18
|
+
});
|
|
19
|
+
return last;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function waitForUrlIncludes(url, needle, options = {}) {
|
|
23
|
+
let body = "";
|
|
24
|
+
await pollUntil(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(url, options.init);
|
|
27
|
+
body = await res.text();
|
|
28
|
+
return body.includes(needle);
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}, {
|
|
33
|
+
intervalMs: options.intervalMs,
|
|
34
|
+
timeoutMs: options.timeoutMs,
|
|
35
|
+
message: options.message ?? `Timed out waiting for "${needle}" from ${url}`
|
|
36
|
+
});
|
|
37
|
+
return body;
|
|
38
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared e2e / integration utilities (the “utils” in untestutils).
|
|
3
|
+
* Browser helpers need optional peer `playwright-core`.
|
|
4
|
+
*/
|
|
5
|
+
export { pollUntil, runSequential, type PollUntilOptions } from "./poll.mjs";
|
|
6
|
+
export { acceptLanguageHeaders, forwardedHostHeaders } from "./headers.mjs";
|
|
7
|
+
export { parseSeoHead, extractSitemapLocs, extractJsonLd, readSeoHead, type SeoHead, type SeoAlternate, type PageLike } from "./seo.mjs";
|
|
8
|
+
export { setLocaleCookie, getLocaleCookie, clearCookies, type CookiePage, type SetCookieOptions } from "./cookies.mjs";
|
|
9
|
+
export { emulateDomain, trackResponses, collectConsole, collectHydrationIssues, setAcceptLanguage, type TrackedResponse, type ConsoleMatch } from "./browser.mjs";
|
|
10
|
+
export { waitForUrlOk, waitForUrlIncludes, type WaitForUrlOptions } from "./http.mjs";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared e2e / integration utilities (the “utils” in untestutils).
|
|
3
|
+
* Browser helpers need optional peer `playwright-core`.
|
|
4
|
+
*/
|
|
5
|
+
export { pollUntil, runSequential, type PollUntilOptions } from './poll';
|
|
6
|
+
export { acceptLanguageHeaders, forwardedHostHeaders } from './headers';
|
|
7
|
+
export { parseSeoHead, extractSitemapLocs, extractJsonLd, readSeoHead, type SeoHead, type SeoAlternate, type PageLike, } from './seo';
|
|
8
|
+
export { setLocaleCookie, getLocaleCookie, clearCookies, type CookiePage, type SetCookieOptions, } from './cookies';
|
|
9
|
+
export { emulateDomain, trackResponses, collectConsole, collectHydrationIssues, setAcceptLanguage, type TrackedResponse, type ConsoleMatch, } from './browser';
|
|
10
|
+
export { waitForUrlOk, waitForUrlIncludes, type WaitForUrlOptions } from './http';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,QAAQ,GACd,MAAM,OAAO,CAAC;AACf,OAAO,EACL,eAAe,EACf,eAAe,EACf,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,aAAa,EACb,cAAc,EACd,cAAc,EACd,sBAAsB,EACtB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,QAAQ,CAAC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
export { pollUntil, runSequential } from "./poll.mjs";
|
|
3
|
+
export { acceptLanguageHeaders, forwardedHostHeaders } from "./headers.mjs";
|
|
4
|
+
export { parseSeoHead, extractSitemapLocs, extractJsonLd, readSeoHead } from "./seo.mjs";
|
|
5
|
+
export { setLocaleCookie, getLocaleCookie, clearCookies } from "./cookies.mjs";
|
|
6
|
+
export { emulateDomain, trackResponses, collectConsole, collectHydrationIssues, setAcceptLanguage } from "./browser.mjs";
|
|
7
|
+
export { waitForUrlOk, waitForUrlIncludes } from "./http.mjs";
|
package/dist/poll.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type PollUntilOptions = {
|
|
2
|
+
intervalMs?: number;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
message?: string;
|
|
5
|
+
};
|
|
6
|
+
/** Poll until `check` returns true or the deadline is reached. */
|
|
7
|
+
export declare function pollUntil(check: () => boolean | Promise<boolean>, options?: PollUntilOptions): Promise<void>;
|
|
8
|
+
/** Run async callbacks one after another (shared Playwright page, etc.). */
|
|
9
|
+
export declare function runSequential<T>(items: readonly T[], fn: (item: T, index: number) => Promise<void>): Promise<void>;
|
package/dist/poll.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type PollUntilOptions = {
|
|
2
|
+
intervalMs?: number;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
message?: string;
|
|
5
|
+
};
|
|
6
|
+
/** Poll until `check` returns true or the deadline is reached. */
|
|
7
|
+
export declare function pollUntil(check: () => boolean | Promise<boolean>, options?: PollUntilOptions): Promise<void>;
|
|
8
|
+
/** Run async callbacks one after another (shared Playwright page, etc.). */
|
|
9
|
+
export declare function runSequential<T>(items: readonly T[], fn: (item: T, index: number) => Promise<void>): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=poll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,kEAAkE;AAClE,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EACvC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,4EAA4E;AAC5E,wBAAsB,aAAa,CAAC,CAAC,EACnC,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
package/dist/poll.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
export async function pollUntil(check, options = {}) {
|
|
3
|
+
const intervalMs = options.intervalMs ?? 250;
|
|
4
|
+
const deadline = Date.now() + (options.timeoutMs ?? 2e4);
|
|
5
|
+
for (;;) {
|
|
6
|
+
if (await check()) return;
|
|
7
|
+
if (Date.now() >= deadline) {
|
|
8
|
+
throw new Error(options.message ?? "Timed out while polling");
|
|
9
|
+
}
|
|
10
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function runSequential(items, fn) {
|
|
15
|
+
let i = 0;
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
await fn(item, i++);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/seo.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SeoAlternate = {
|
|
2
|
+
hreflang: string;
|
|
3
|
+
href: string;
|
|
4
|
+
};
|
|
5
|
+
export type SeoHead = {
|
|
6
|
+
canonical: string | null;
|
|
7
|
+
ogUrl: string | null;
|
|
8
|
+
ogLocale: string | null;
|
|
9
|
+
hreflangs: SeoAlternate[];
|
|
10
|
+
};
|
|
11
|
+
/** Parse common SEO tags from an HTML document string (SSR `$fetch` / `request.get().text()`). */
|
|
12
|
+
export declare function parseSeoHead(html: string): SeoHead;
|
|
13
|
+
/** `<loc>` values from a sitemap XML body. */
|
|
14
|
+
export declare function extractSitemapLocs(xml: string): string[];
|
|
15
|
+
/** JSON-LD objects from `<script type="application/ld+json">` blocks. */
|
|
16
|
+
export declare function extractJsonLd(html: string): unknown[];
|
|
17
|
+
export type PageLike = {
|
|
18
|
+
locator: (selector: string) => {
|
|
19
|
+
getAttribute: (name: string) => Promise<string | null>;
|
|
20
|
+
count: () => Promise<number>;
|
|
21
|
+
nth: (i: number) => {
|
|
22
|
+
getAttribute: (name: string) => Promise<string | null>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** Read SEO tags from a live Playwright page (after goto). */
|
|
27
|
+
export declare function readSeoHead(page: PageLike): Promise<SeoHead>;
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SeoAlternate = {
|
|
2
|
+
hreflang: string;
|
|
3
|
+
href: string;
|
|
4
|
+
};
|
|
5
|
+
export type SeoHead = {
|
|
6
|
+
canonical: string | null;
|
|
7
|
+
ogUrl: string | null;
|
|
8
|
+
ogLocale: string | null;
|
|
9
|
+
hreflangs: SeoAlternate[];
|
|
10
|
+
};
|
|
11
|
+
/** Parse common SEO tags from an HTML document string (SSR `$fetch` / `request.get().text()`). */
|
|
12
|
+
export declare function parseSeoHead(html: string): SeoHead;
|
|
13
|
+
/** `<loc>` values from a sitemap XML body. */
|
|
14
|
+
export declare function extractSitemapLocs(xml: string): string[];
|
|
15
|
+
/** JSON-LD objects from `<script type="application/ld+json">` blocks. */
|
|
16
|
+
export declare function extractJsonLd(html: string): unknown[];
|
|
17
|
+
export type PageLike = {
|
|
18
|
+
locator: (selector: string) => {
|
|
19
|
+
getAttribute: (name: string) => Promise<string | null>;
|
|
20
|
+
count: () => Promise<number>;
|
|
21
|
+
nth: (i: number) => {
|
|
22
|
+
getAttribute: (name: string) => Promise<string | null>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** Read SEO tags from a live Playwright page (after goto). */
|
|
27
|
+
export declare function readSeoHead(page: PageLike): Promise<SeoHead>;
|
|
28
|
+
//# sourceMappingURL=seo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../src/seo.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,OAAO,GAAG;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,YAAY,EAAE,CAAC;CAC3B,CAAC;AAMF,kGAAkG;AAClG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAyBlD;AAED,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAYrD;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK;QAC7B,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACvD,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK;YAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;SAAE,CAAC;KAChF,CAAC;CACH,CAAC;AAEF,8DAA8D;AAC9D,wBAAsB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBlE"}
|
package/dist/seo.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function attr(tag, name) {
|
|
2
|
+
return tag.match(new RegExp(`${name}="([^"]*)"`, "i"))?.[1] ?? null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function parseSeoHead(html) {
|
|
6
|
+
const canonical = html.match(/<link[^>]*rel=["']canonical["'][^>]*>/i)?.[0] !== undefined ? attr(html.match(/<link[^>]*rel=["']canonical["'][^>]*>/i)[0], "href") : null;
|
|
7
|
+
let ogUrl = null;
|
|
8
|
+
let ogLocale = null;
|
|
9
|
+
for (const m of html.matchAll(/<meta\b[^>]*>/gi)) {
|
|
10
|
+
const tag = m[0];
|
|
11
|
+
const prop = attr(tag, "property") ?? attr(tag, "name");
|
|
12
|
+
if (prop === "og:url") ogUrl = attr(tag, "content");
|
|
13
|
+
if (prop === "og:locale") ogLocale = attr(tag, "content");
|
|
14
|
+
}
|
|
15
|
+
const hreflangs = [];
|
|
16
|
+
for (const m of html.matchAll(/<link\b[^>]*>/gi)) {
|
|
17
|
+
const tag = m[0];
|
|
18
|
+
if (!/\brel=["']alternate["']/i.test(tag)) continue;
|
|
19
|
+
const hreflang = attr(tag, "hreflang");
|
|
20
|
+
const href = attr(tag, "href");
|
|
21
|
+
if (hreflang && href) hreflangs.push({
|
|
22
|
+
hreflang,
|
|
23
|
+
href
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
canonical,
|
|
28
|
+
ogUrl,
|
|
29
|
+
ogLocale,
|
|
30
|
+
hreflangs
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function extractSitemapLocs(xml) {
|
|
35
|
+
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractJsonLd(html) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const m of html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
41
|
+
try {
|
|
42
|
+
out.push(JSON.parse(m[1]));
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readSeoHead(page) {
|
|
49
|
+
const canonical = await page.locator("link[rel=\"canonical\"]").getAttribute("href");
|
|
50
|
+
const ogUrl = await page.locator("meta[property=\"og:url\"]").getAttribute("content");
|
|
51
|
+
const ogLocale = await page.locator("meta[property=\"og:locale\"]").getAttribute("content");
|
|
52
|
+
const alt = page.locator("link[rel=\"alternate\"][hreflang]");
|
|
53
|
+
const n = await alt.count();
|
|
54
|
+
const hreflangs = [];
|
|
55
|
+
for (let i = 0; i < n; i++) {
|
|
56
|
+
const el = alt.nth(i);
|
|
57
|
+
const hreflang = await el.getAttribute("hreflang");
|
|
58
|
+
const href = await el.getAttribute("href");
|
|
59
|
+
if (hreflang && href) hreflangs.push({
|
|
60
|
+
hreflang,
|
|
61
|
+
href
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
canonical,
|
|
66
|
+
ogUrl,
|
|
67
|
+
ogLocale,
|
|
68
|
+
hreflangs
|
|
69
|
+
};
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@untestutils/utils",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Shared e2e/test utilities — cookies, SEO head, poll, browser helpers",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"default": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^26.6.1",
|
|
20
|
+
"playwright-core": "^1.63.0",
|
|
21
|
+
"publint": "^0.3.24",
|
|
22
|
+
"typescript": "^6.0.3",
|
|
23
|
+
"obuild": "^0.4.40"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"playwright-core": "^1.63.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"playwright-core": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://s00d.github.io/untestutils/",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/s00d/untestutils.git",
|
|
41
|
+
"directory": "packages/utils"
|
|
42
|
+
},
|
|
43
|
+
"main": "./dist/index.mjs",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
50
|
+
"build": "obuild && tsc -p tsconfig.build.json",
|
|
51
|
+
"publint": "publint --strict"
|
|
52
|
+
}
|
|
53
|
+
}
|