@manufac/general-tests 0.5.0 → 0.9.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 +88 -0
- package/dist/config/projects.d.ts +3 -1
- package/dist/config/projects.js +4 -2
- package/dist/fixtures/index.d.ts +2 -1
- package/dist/fixtures/index.js +1 -0
- package/dist/schema/index.d.ts +13 -0
- package/dist/schema/index.js +7 -0
- package/dist/specs/cta-destinations.spec.js +65 -0
- package/dist/specs/page-visual-regression.spec.js +68 -0
- package/dist/specs/{visual-regression.spec.js → storybook-visual-regression.spec.js} +7 -0
- package/dist/specs/utils.d.ts +4 -158
- package/dist/specs/utils.js +31 -857
- package/package.json +1 -1
- package/dist/specs/desktop-only/browser-zoom.spec.js +0 -26
- package/dist/specs/mobile-only/ios-safari-toolbar.spec.js +0 -62
- package/dist/specs/mobile-only/ios-safe-areas.spec.js +0 -36
- package/dist/specs/mobile-only/navigation.spec.d.ts +0 -1
- package/dist/specs/mobile-only/navigation.spec.js +0 -40
- package/dist/specs/mobile-only/orientation.spec.d.ts +0 -1
- package/dist/specs/mobile-only/orientation.spec.js +0 -46
- package/dist/specs/mobile-only/readability.spec.d.ts +0 -1
- package/dist/specs/mobile-only/readability.spec.js +0 -14
- package/dist/specs/mobile-only/soft-keyboard.spec.d.ts +0 -1
- package/dist/specs/mobile-only/soft-keyboard.spec.js +0 -43
- package/dist/specs/mobile-only/touch-targets.spec.d.ts +0 -1
- package/dist/specs/mobile-only/touch-targets.spec.js +0 -12
- package/dist/specs/shared/cta.spec.d.ts +0 -1
- package/dist/specs/shared/cta.spec.js +0 -199
- package/dist/specs/shared/forms/autofill.spec.d.ts +0 -1
- package/dist/specs/shared/forms/autofill.spec.js +0 -28
- package/dist/specs/shared/forms/double-submit.spec.d.ts +0 -1
- package/dist/specs/shared/forms/double-submit.spec.js +0 -25
- package/dist/specs/shared/forms/input-edge.spec.d.ts +0 -1
- package/dist/specs/shared/forms/input-edge.spec.js +0 -60
- package/dist/specs/shared/forms/invalid-input.spec.d.ts +0 -1
- package/dist/specs/shared/forms/invalid-input.spec.js +0 -41
- package/dist/specs/shared/forms/native-inputs.spec.d.ts +0 -1
- package/dist/specs/shared/forms/native-inputs.spec.js +0 -37
- package/dist/specs/shared/forms/required-field.spec.d.ts +0 -1
- package/dist/specs/shared/forms/required-field.spec.js +0 -39
- package/dist/specs/shared/forms/thankyou-confirm.spec.d.ts +0 -1
- package/dist/specs/shared/forms/thankyou-confirm.spec.js +0 -34
- package/dist/specs/shared/hero-fold.spec.d.ts +0 -1
- package/dist/specs/shared/hero-fold.spec.js +0 -40
- package/dist/specs/shared/image-quality.spec.d.ts +0 -1
- package/dist/specs/shared/image-quality.spec.js +0 -13
- package/dist/specs/shared/media-embeds/carousel-slider.spec.d.ts +0 -1
- package/dist/specs/shared/media-embeds/carousel-slider.spec.js +0 -31
- package/dist/specs/shared/media-embeds/cookie-consent.spec.d.ts +0 -1
- package/dist/specs/shared/media-embeds/cookie-consent.spec.js +0 -27
- package/dist/specs/shared/media-embeds/third-party-embeds.spec.d.ts +0 -1
- package/dist/specs/shared/media-embeds/third-party-embeds.spec.js +0 -27
- package/dist/specs/shared/media-embeds/video-playback.spec.d.ts +0 -1
- package/dist/specs/shared/media-embeds/video-playback.spec.js +0 -54
- package/dist/specs/shared/sticky-elements.spec.d.ts +0 -1
- package/dist/specs/shared/sticky-elements.spec.js +0 -13
- package/dist/specs/sticky-utils.d.ts +0 -8
- package/dist/specs/sticky-utils.js +0 -175
- package/dist/specs/visual-regression.spec.d.ts +0 -1
- /package/dist/specs/{desktop-only/browser-zoom.spec.d.ts → cta-destinations.spec.d.ts} +0 -0
- /package/dist/specs/{mobile-only/ios-safari-toolbar.spec.d.ts → page-visual-regression.spec.d.ts} +0 -0
- /package/dist/specs/{mobile-only/ios-safe-areas.spec.d.ts → storybook-visual-regression.spec.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -134,3 +134,91 @@ Then add two scripts to the app's `package.json` (adjust `start:sb`/`build:sb` t
|
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
Baselines land in `e2e-visual/__screenshots__/<story-id>.png`, next to `playwright.storybook.config.ts`.
|
|
137
|
+
|
|
138
|
+
## Full-page visual regression
|
|
139
|
+
|
|
140
|
+
`PageVisualRegressionTestMatch` runs `page-visual-regression.spec.ts` — one generic test that visits every page in `Config.pages` and compares a full-page screenshot against a saved baseline with `toHaveScreenshot()`. A page path becomes the snapshot name by trimming slashes (`/` -> `home`, `/pricing` -> `pricing`, `/docs/setup` -> `docs-setup`). Failures are collected, so one broken page doesn't hide the others.
|
|
141
|
+
|
|
142
|
+
### Setup
|
|
143
|
+
|
|
144
|
+
Run it in its own project, so it starts only after the other projects finish. Full-page screenshots of long pages are slow and sensitive to load, and running them next to other tests makes them flaky. Projects that list each other in `dependencies` run one after another, so only one visual project runs at a time.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { LoadingPageTestMatch, PageVisualRegressionTestMatch } from "@manufac/general-tests/config/projects.js";
|
|
148
|
+
|
|
149
|
+
const Projects: Project[] = [
|
|
150
|
+
{
|
|
151
|
+
name: "Desktop Chrome",
|
|
152
|
+
testDir: GeneralTestsSpecsDir,
|
|
153
|
+
testMatch: [LoadingPageTestMatch],
|
|
154
|
+
use: { ...devices["Desktop Chrome"], viewport: DesktopBreakpoints.Desktop },
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "Desktop Chrome Visual",
|
|
158
|
+
testDir: GeneralTestsSpecsDir,
|
|
159
|
+
testMatch: PageVisualRegressionTestMatch,
|
|
160
|
+
dependencies: ["Desktop Chrome"],
|
|
161
|
+
snapshotPathTemplate: "e2e/__screenshots__/Desktop-Chrome/{arg}{ext}",
|
|
162
|
+
use: { ...devices["Desktop Chrome"], viewport: DesktopBreakpoints.Desktop },
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
// Chain the next visual project onto the previous one.
|
|
166
|
+
name: "Responsive 375 Visual",
|
|
167
|
+
testDir: GeneralTestsSpecsDir,
|
|
168
|
+
testMatch: PageVisualRegressionTestMatch,
|
|
169
|
+
dependencies: ["Desktop Chrome Visual"],
|
|
170
|
+
snapshotPathTemplate: "e2e/__screenshots__/Responsive-375/{arg}{ext}",
|
|
171
|
+
use: { ...devices["Desktop Chrome"], viewport: ResponsiveLayoutBreakpoints.MobileL },
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
export default defineConfig<PageTestOptions>({
|
|
176
|
+
testDir: "./e2e",
|
|
177
|
+
expect: { timeout: 10_000, toHaveScreenshot: { maxDiffPixelRatio: 0.001, animations: "disabled" } },
|
|
178
|
+
use: { baseURL: "https://your-app.example.com", pages: Config.pages, actionTimeout: 10_000 },
|
|
179
|
+
projects: Projects,
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- Each visual project has its own `snapshotPathTemplate`, so baselines are kept per project (Desktop Chrome and 375 differ in layout) and stay in the repo.
|
|
184
|
+
- The image and video waits have no timeout of their own. They use `use.actionTimeout`, so one stuck image fails that page instead of hanging the suite.
|
|
185
|
+
- `workers` is a global Playwright setting and can't be set per project. Chaining the projects gives the same result for the visual ones, because each has a single test and only one project runs at a time.
|
|
186
|
+
- If a project in `dependencies` fails, the visual projects are skipped. Use `--no-deps` to run them on their own.
|
|
187
|
+
|
|
188
|
+
### Scripts
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"scripts": {
|
|
193
|
+
"test:visual:pages": "playwright test page-visual-regression --no-deps --workers=1 --project=\"* Visual\"",
|
|
194
|
+
"update:visual:pages": "npm run test:visual:pages -- --update-snapshots=all"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- The first run has no baselines, so use `update:visual:pages`.
|
|
200
|
+
- Use `--update-snapshots=all` to rewrite every baseline. A plain `--update-snapshots` only rewrites the ones that fail the comparison, so old files can stay behind.
|
|
201
|
+
- Review the new images before committing them. Check that the header is at the top and the video area is a solid box.
|
|
202
|
+
- Don't run two Playwright runs at once in the same app. They share `test-results`, and one run can delete the other's files.
|
|
203
|
+
|
|
204
|
+
## CTA & link destination checks
|
|
205
|
+
|
|
206
|
+
`CTADestinationsTestMatch` runs `cta-destinations.spec.ts` — `CT-01` clicks each configured CTA and verifies it lands on its expected destination (same tab by default, or a new tab when `opensInNewTab` is set); `CT-02` visits every internal link reachable from `Config.pages` and checks it isn't a 404 or blank page, then verifies each configured legal link still points to its expected URL.
|
|
207
|
+
|
|
208
|
+
Both are driven entirely by config, since the shared package has no way to know an individual app's design spec on its own:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// manufac-test.config.ts
|
|
212
|
+
import type { ManufacTestConfig } from "@manufac/general-tests/schema/index.js";
|
|
213
|
+
|
|
214
|
+
export const Config: ManufacTestConfig = {
|
|
215
|
+
pages: ["/", "/pricing", "/contact-us", "/privacy"],
|
|
216
|
+
ctas: [
|
|
217
|
+
{ ctaID: "hero-cta", page: "/", name: "Get Started", expectedHref: "/contact-us" },
|
|
218
|
+
{ ctaID: "pricing-cta", page: "/pricing", name: "Book a demo", expectedHref: "/contact-us", opensInNewTab: true },
|
|
219
|
+
],
|
|
220
|
+
legalLinks: [{ page: "/", name: "Privacy Policy", expectedHref: "/privacy" }],
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Add `CTADestinationsTestMatch` to a project's `testMatch` the same way as `LoadingPageTestMatch`. `CT-01` skips entirely if `ctas` is left empty.
|
|
@@ -35,4 +35,6 @@ export declare const ProjectTypes: {
|
|
|
35
35
|
export declare const LoadingPageTestMatch = "loading-page.spec.js";
|
|
36
36
|
export declare const ResponsiveLayoutTestMatch = "responsive-layout.spec.js";
|
|
37
37
|
export declare const FormsTestMatch = "forms.spec.js";
|
|
38
|
-
export declare const
|
|
38
|
+
export declare const StorybookVisualRegressionTestMatch = "storybook-visual-regression.spec.js";
|
|
39
|
+
export declare const CTADestinationsTestMatch = "cta-destinations.spec.js";
|
|
40
|
+
export declare const PageVisualRegressionTestMatch = "page-visual-regression.spec.js";
|
package/dist/config/projects.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.PageVisualRegressionTestMatch = exports.CTADestinationsTestMatch = exports.StorybookVisualRegressionTestMatch = exports.FormsTestMatch = exports.ResponsiveLayoutTestMatch = exports.LoadingPageTestMatch = exports.ProjectTypes = exports.ResponsiveLayoutBreakpoints = exports.DesktopBreakpoints = void 0;
|
|
4
4
|
// Section 3 of the Landing Page Test Plan — breakpoints / viewports.
|
|
5
5
|
exports.DesktopBreakpoints = {
|
|
6
6
|
Desktop: { name: "Desktop", width: 1440, height: 900 },
|
|
@@ -18,4 +18,6 @@ exports.ProjectTypes = {
|
|
|
18
18
|
exports.LoadingPageTestMatch = "loading-page.spec.js";
|
|
19
19
|
exports.ResponsiveLayoutTestMatch = "responsive-layout.spec.js";
|
|
20
20
|
exports.FormsTestMatch = "forms.spec.js";
|
|
21
|
-
exports.
|
|
21
|
+
exports.StorybookVisualRegressionTestMatch = "storybook-visual-regression.spec.js";
|
|
22
|
+
exports.CTADestinationsTestMatch = "cta-destinations.spec.js";
|
|
23
|
+
exports.PageVisualRegressionTestMatch = "page-visual-regression.spec.js";
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ManufacTestFormConfig } from "../schema";
|
|
1
|
+
import type { ManufacTestCTA, ManufacTestFormConfig } from "../schema";
|
|
2
2
|
export interface PageTestOptions {
|
|
3
3
|
targetPage: string;
|
|
4
4
|
pages: string[];
|
|
5
5
|
forms: ManufacTestFormConfig[];
|
|
6
|
+
ctas: ManufacTestCTA[];
|
|
6
7
|
consoleErrors: string[];
|
|
7
8
|
}
|
|
8
9
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & PageTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
package/dist/fixtures/index.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.test = test_1.test.extend({
|
|
|
9
9
|
targetPage: ["/", { option: true }],
|
|
10
10
|
pages: [["/"], { option: true }],
|
|
11
11
|
forms: [[], { option: true }],
|
|
12
|
+
ctas: [[], { option: true }],
|
|
12
13
|
consoleErrors: async ({ page }, use) => {
|
|
13
14
|
const errors = [];
|
|
14
15
|
page.on("console", (message) => {
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -70,6 +70,13 @@ export declare const ManufacTestFormsSchema: z.ZodArray<z.ZodObject<{
|
|
|
70
70
|
}, z.core.$strip>>;
|
|
71
71
|
successMessage: z.ZodString;
|
|
72
72
|
}, z.core.$strip>>;
|
|
73
|
+
declare const CTASchema: z.ZodObject<{
|
|
74
|
+
ctaID: z.ZodString;
|
|
75
|
+
page: z.ZodString;
|
|
76
|
+
expectedHref: z.ZodString;
|
|
77
|
+
shouldOpenInNewTab: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
export type ManufacTestCTA = z.infer<typeof CTASchema>;
|
|
73
80
|
export declare const ManufacTestConfigSchema: z.ZodObject<{
|
|
74
81
|
pages: z.ZodArray<z.ZodString>;
|
|
75
82
|
forms: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
@@ -91,6 +98,12 @@ export declare const ManufacTestConfigSchema: z.ZodObject<{
|
|
|
91
98
|
}, z.core.$strip>>;
|
|
92
99
|
successMessage: z.ZodString;
|
|
93
100
|
}, z.core.$strip>>>>;
|
|
101
|
+
ctas: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
102
|
+
ctaID: z.ZodString;
|
|
103
|
+
page: z.ZodString;
|
|
104
|
+
expectedHref: z.ZodString;
|
|
105
|
+
shouldOpenInNewTab: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
106
|
+
}, z.core.$strip>>>>;
|
|
94
107
|
}, z.core.$strip>;
|
|
95
108
|
export type ManufacTestConfig = z.infer<typeof ManufacTestConfigSchema>;
|
|
96
109
|
export declare const StorybookIndexSchema: z.ZodObject<{
|
package/dist/schema/index.js
CHANGED
|
@@ -34,9 +34,16 @@ const FormSchema = zod_1.z.object({
|
|
|
34
34
|
});
|
|
35
35
|
/** Validates the `forms` fixture option. */
|
|
36
36
|
exports.ManufacTestFormsSchema = zod_1.z.array(FormSchema);
|
|
37
|
+
const CTASchema = zod_1.z.object({
|
|
38
|
+
ctaID: zod_1.z.string().min(1),
|
|
39
|
+
page: zod_1.z.string().min(1),
|
|
40
|
+
expectedHref: zod_1.z.string().min(1),
|
|
41
|
+
shouldOpenInNewTab: zod_1.z.boolean().nullish(),
|
|
42
|
+
});
|
|
37
43
|
exports.ManufacTestConfigSchema = zod_1.z.object({
|
|
38
44
|
pages: zod_1.z.array(zod_1.z.string()).min(1, "At least one page is required"),
|
|
39
45
|
forms: zod_1.z.array(FormSchema).nullish(),
|
|
46
|
+
ctas: zod_1.z.array(CTASchema).nullish(),
|
|
40
47
|
});
|
|
41
48
|
exports.StorybookIndexSchema = zod_1.z.object({
|
|
42
49
|
entries: zod_1.z.record(zod_1.z.string(), zod_1.z.object({
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fixtures_1 = require("../fixtures");
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
fixtures_1.test.describe("Landing Page - CTA & Link Destinations", () => {
|
|
6
|
+
(0, fixtures_1.test)("CT-01: All CTAs functional", async ({ page, ctas }) => {
|
|
7
|
+
for (const cta of ctas) {
|
|
8
|
+
await fixtures_1.test.step(cta.ctaID, async () => {
|
|
9
|
+
await page.goto(cta.page);
|
|
10
|
+
const locator = page.getByTestId(cta.ctaID);
|
|
11
|
+
await (0, test_1.expect)(locator, `CTA "${cta.ctaID}" should be visible`).toBeVisible();
|
|
12
|
+
const shouldOpenNewTab = cta.shouldOpenInNewTab === true;
|
|
13
|
+
const [popup] = await Promise.all([
|
|
14
|
+
shouldOpenNewTab ? page.waitForEvent("popup") : Promise.resolve(null),
|
|
15
|
+
locator.click(),
|
|
16
|
+
]);
|
|
17
|
+
const destination = popup ?? page;
|
|
18
|
+
await (0, test_1.expect)(destination, `CTA "${cta.ctaID}" should navigate to ${cta.expectedHref}`).toHaveURL(new RegExp(cta.expectedHref));
|
|
19
|
+
await popup?.close();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
(0, fixtures_1.test)("CT-02: All links valid", async ({ page, pages }) => {
|
|
24
|
+
fixtures_1.test.slow();
|
|
25
|
+
for (const targetPage of pages) {
|
|
26
|
+
await fixtures_1.test.step(targetPage, async () => {
|
|
27
|
+
await page.goto(targetPage);
|
|
28
|
+
const links = page.getByRole("link");
|
|
29
|
+
const count = await links.count();
|
|
30
|
+
(0, test_1.expect)(count, `Page ${targetPage} should have at least some links`).toBeGreaterThan(0);
|
|
31
|
+
const hrefs = new Set();
|
|
32
|
+
for (let index = 0; index < count; index++) {
|
|
33
|
+
const href = await links.nth(index).getAttribute("href");
|
|
34
|
+
// Get in app hrefs
|
|
35
|
+
if (typeof href === "string" && href.startsWith("/")) {
|
|
36
|
+
hrefs.add(href);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const href of hrefs) {
|
|
40
|
+
await fixtures_1.test.step(href, async () => {
|
|
41
|
+
const hashIndex = href.indexOf("#");
|
|
42
|
+
const hasHash = hashIndex !== -1;
|
|
43
|
+
const hrefPath = hasHash ? href.slice(0, hashIndex) : href;
|
|
44
|
+
const hrefHash = hasHash ? href.slice(hashIndex + 1) : "";
|
|
45
|
+
const currentPath = new URL(page.url()).pathname;
|
|
46
|
+
// An in-page anchor (same path, just a hash) doesn't trigger a navigation, so
|
|
47
|
+
// page.goto() returns no response — checking it against the DOM is what matters.
|
|
48
|
+
const isSameDocumentAnchor = hasHash && (hrefPath === "" || hrefPath === currentPath);
|
|
49
|
+
if (isSameDocumentAnchor && hrefHash.length > 0) {
|
|
50
|
+
const targetCount = await page.locator(`#${hrefHash}`).count(); // same page navigation happens by HTML "id" field
|
|
51
|
+
(0, test_1.expect)(targetCount > 0, `Anchor link ${href} should point to an existing element`).toBe(true);
|
|
52
|
+
}
|
|
53
|
+
else if (!isSameDocumentAnchor) {
|
|
54
|
+
const response = await page.goto(href);
|
|
55
|
+
(0, test_1.expect)(response?.ok(), `Link ${href} should load without a 404/500`).toBe(true);
|
|
56
|
+
await (0, test_1.expect)(page.getByText("Page not found"), `Link ${href} should not land on a 404 page`).toBeHidden();
|
|
57
|
+
const bodyText = await page.locator("body").innerText();
|
|
58
|
+
(0, test_1.expect)(bodyText.trim().length > 0, `Link ${href} should not render a blank page`).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("./utils");
|
|
4
|
+
const fixtures_1 = require("../fixtures");
|
|
5
|
+
const test_1 = require("@playwright/test");
|
|
6
|
+
(0, fixtures_1.test)("page screenshots match their baseline", async ({ page, pages }) => {
|
|
7
|
+
// One test per page would leave too little time at the default 30s — triple it.
|
|
8
|
+
fixtures_1.test.slow();
|
|
9
|
+
const errors = [];
|
|
10
|
+
for (const targetPage of pages) {
|
|
11
|
+
await fixtures_1.test.step(targetPage, async () => {
|
|
12
|
+
try {
|
|
13
|
+
await page.goto(targetPage);
|
|
14
|
+
await page.waitForLoadState("networkidle");
|
|
15
|
+
// Triggers lazy-loaded images below the fold before scrolling back up.
|
|
16
|
+
await (0, utils_1.scrollThroughPage)(page);
|
|
17
|
+
// `behavior: "instant"` skips the page's smooth scrolling. Waiting for scrollY 0 matters because
|
|
18
|
+
// sticky/fixed elements (like the header) are painted wherever the page is scrolled to.
|
|
19
|
+
await page.evaluate(() => {
|
|
20
|
+
window.scrollTo({ top: 0, behavior: "instant" });
|
|
21
|
+
});
|
|
22
|
+
await test_1.expect
|
|
23
|
+
.poll(() => {
|
|
24
|
+
return page.evaluate(() => {
|
|
25
|
+
return window.scrollY;
|
|
26
|
+
});
|
|
27
|
+
})
|
|
28
|
+
.toBe(0);
|
|
29
|
+
// Some lazy images are never scrolled into view (e.g. hidden at this breakpoint) and would never load.
|
|
30
|
+
await page.evaluate(() => {
|
|
31
|
+
for (const image of document.querySelectorAll("img[loading='lazy']")) {
|
|
32
|
+
image.loading = "eager";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Timeout comes from the consuming app's `use.actionTimeout`, so a stuck image fails
|
|
36
|
+
// just this page instead of hanging the whole suite.
|
|
37
|
+
await page.waitForFunction(() => {
|
|
38
|
+
return Array.from(document.images).every((image) => {
|
|
39
|
+
return image.complete;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
// `complete` only means the file arrived; decode() waits until the image can be painted in full.
|
|
43
|
+
await page.evaluate(async () => {
|
|
44
|
+
await Promise.all(Array.from(document.images).map((image) => {
|
|
45
|
+
return image.decode().catch(() => undefined);
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
// A video's box size depends on its metadata, so wait for it or the mask height changes between runs.
|
|
49
|
+
await page.waitForFunction(() => {
|
|
50
|
+
return Array.from(document.querySelectorAll("video")).every((video) => {
|
|
51
|
+
return video.readyState >= HTMLMediaElement.HAVE_METADATA;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// A playing video changes every frame and `animations: "disabled"` doesn't stop it, so mask it.
|
|
55
|
+
await (0, test_1.expect)(page).toHaveScreenshot(`${(0, utils_1.toSnapshotName)(targetPage)}.png`, {
|
|
56
|
+
fullPage: true,
|
|
57
|
+
mask: [page.locator("video")],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Collect failures instead of stopping at the first broken page.
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
errors.push(`Page ${targetPage} failed: ${message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
(0, test_1.expect)(errors, `Visual regression failures:\n${errors.join("\n")}`).toHaveLength(0);
|
|
68
|
+
});
|
|
@@ -24,6 +24,13 @@ const test_1 = require("@playwright/test");
|
|
|
24
24
|
await page.goto(`iframe.html?id=${storyID}&viewMode=story`);
|
|
25
25
|
await (0, test_1.expect)(page.locator("#storybook-root")).toBeVisible();
|
|
26
26
|
await (0, test_1.expect)(page.locator(".sb-preparing-story .sb-loader")).toBeHidden(); // Avoid screenshotting a half-rendered story
|
|
27
|
+
// Ensure all images are fully loaded before taking the visual regression screenshot.
|
|
28
|
+
// Ref: https://playwright.dev/docs/api/class-page#page-wait-for-function
|
|
29
|
+
await page.waitForFunction(() => {
|
|
30
|
+
return Array.from(document.images).every((image) => {
|
|
31
|
+
return image.complete;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
27
34
|
await (0, test_1.expect)(page).toHaveScreenshot(`${storyID}.png`, { fullPage: true, timeout: 10_000 });
|
|
28
35
|
}
|
|
29
36
|
catch (error) {
|
package/dist/specs/utils.d.ts
CHANGED
|
@@ -1,162 +1,5 @@
|
|
|
1
|
+
import { type ManufacTestFormField } from "../schema";
|
|
1
2
|
import { type Locator, type Page } from "@playwright/test";
|
|
2
|
-
import type { ManufacTestFormField } from "../schema";
|
|
3
|
-
export declare const ResponsiveConfig: {
|
|
4
|
-
readonly ScrollSteps: 10;
|
|
5
|
-
readonly ScrollStepDelayMs: 100;
|
|
6
|
-
readonly ViewPortWidth: 1440;
|
|
7
|
-
readonly ViewPortHeight: 900;
|
|
8
|
-
readonly ZoomFactor: 2;
|
|
9
|
-
readonly ZoomSettleDelayMs: 300;
|
|
10
|
-
readonly UpscaleTolerance: 1.05;
|
|
11
|
-
readonly OversizeTolerance: 2;
|
|
12
|
-
readonly ScrollDriftTolerance: 0.15;
|
|
13
|
-
readonly MinBodyTextFontSizePX: 12;
|
|
14
|
-
readonly PinFlickerTolerancePX: 2;
|
|
15
|
-
readonly MinTouchTargetSizePX: 44;
|
|
16
|
-
readonly KeyboardOcclusionRatio: 0.4;
|
|
17
|
-
readonly CutoffTolerancePX: 5;
|
|
18
|
-
};
|
|
19
|
-
export interface ScrollMetrics {
|
|
20
|
-
scrollHeight: number;
|
|
21
|
-
scrollY: number;
|
|
22
|
-
clientHeight: number;
|
|
23
|
-
}
|
|
24
|
-
export interface Viewport {
|
|
25
|
-
name: string;
|
|
26
|
-
width: number;
|
|
27
|
-
height: number;
|
|
28
|
-
}
|
|
29
|
-
export interface ElementDescriptor {
|
|
30
|
-
tagName: string;
|
|
31
|
-
className: string;
|
|
32
|
-
}
|
|
33
|
-
export declare function describeElement(descriptor: ElementDescriptor): string;
|
|
34
|
-
export interface ElementOverlap {
|
|
35
|
-
elementA: ElementDescriptor;
|
|
36
|
-
elementB: ElementDescriptor;
|
|
37
|
-
overlapWidth: number;
|
|
38
|
-
overlapHeight: number;
|
|
39
|
-
}
|
|
40
|
-
export declare const TappableElementsSelector: string;
|
|
41
|
-
export declare const TouchTargetSizeSelector: string;
|
|
42
|
-
export declare function getScrollMetrics(page: Page): Promise<ScrollMetrics>;
|
|
43
|
-
export declare function scrollThroughPage(page: Page): Promise<void>;
|
|
44
|
-
export declare function findTruncatedOrOverflowingText(page: Page): Promise<string[]>;
|
|
45
|
-
export declare function findOverlappingElements(page: Page, containerSelector: string): Promise<string[]>;
|
|
46
|
-
export declare const ContentOverlapSelector: string;
|
|
47
|
-
export interface ImageSizeIssue {
|
|
48
|
-
src: string;
|
|
49
|
-
naturalWidth: number;
|
|
50
|
-
naturalHeight: number;
|
|
51
|
-
renderedWidth: number;
|
|
52
|
-
renderedHeight: number;
|
|
53
|
-
devicePixelRatio: number;
|
|
54
|
-
}
|
|
55
|
-
export declare function findImageSizeIssues(page: Page): Promise<ImageSizeIssue[]>;
|
|
56
|
-
export declare function isImageUpscaled(image: ImageSizeIssue): boolean;
|
|
57
|
-
export declare function isImageOversized(image: ImageSizeIssue): boolean;
|
|
58
|
-
export interface ElementRect {
|
|
59
|
-
left: number;
|
|
60
|
-
right: number;
|
|
61
|
-
top: number;
|
|
62
|
-
bottom: number;
|
|
63
|
-
width: number;
|
|
64
|
-
height: number;
|
|
65
|
-
}
|
|
66
|
-
export interface ViewportSize {
|
|
67
|
-
width: number;
|
|
68
|
-
height: number;
|
|
69
|
-
}
|
|
70
|
-
interface RenderedElementSnapshot extends ElementDescriptor {
|
|
71
|
-
rect: ElementRect;
|
|
72
|
-
isFixedOrSticky: boolean;
|
|
73
|
-
}
|
|
74
|
-
export declare function isCutoffAtViewportEdge(snapshot: RenderedElementSnapshot, viewport: ViewportSize): boolean;
|
|
75
|
-
export declare function findElementsCutoffAtViewportEdge(page: Page, containerSelector?: string): Promise<string[]>;
|
|
76
|
-
export declare function findUndersizedBodyText(page: Page): Promise<string[]>;
|
|
77
|
-
export declare function findUndersizedTouchTargets(page: Page, containerSelector: string): Promise<string[]>;
|
|
78
|
-
export declare function hasHorizontalScrollbar(page: Page): Promise<boolean>;
|
|
79
|
-
export declare function isVideoAutoplaying(video: Locator): Promise<boolean>;
|
|
80
|
-
export declare function isVideoMuted(video: Locator): Promise<boolean>;
|
|
81
|
-
export interface UserInfoShape {
|
|
82
|
-
ValidTestName: string;
|
|
83
|
-
ValidTestEmail: string;
|
|
84
|
-
ValidTestPhone: string;
|
|
85
|
-
ValidTestCompany: string;
|
|
86
|
-
ValidTestProjectDescription: string;
|
|
87
|
-
ValidTestPreferredDate: string;
|
|
88
|
-
ValidTestPreferredTime: string;
|
|
89
|
-
LongNameCharacterCount: number;
|
|
90
|
-
EmojiTestName: string;
|
|
91
|
-
PaddedTestEmail: string;
|
|
92
|
-
InvalidTestEmail: string;
|
|
93
|
-
}
|
|
94
|
-
export declare const ValidUserInfo: UserInfoShape;
|
|
95
|
-
export interface RequiredField {
|
|
96
|
-
label: RegExp;
|
|
97
|
-
validValue: string;
|
|
98
|
-
}
|
|
99
|
-
export declare function isFieldRequired(fieldLocator: Locator): Promise<boolean>;
|
|
100
|
-
export declare const FieldInteractions: {
|
|
101
|
-
readonly Fill: "fill";
|
|
102
|
-
readonly Datetime: "datetime";
|
|
103
|
-
};
|
|
104
|
-
export type FieldInteraction = (typeof FieldInteractions)[keyof typeof FieldInteractions];
|
|
105
|
-
export interface FieldValueMapping {
|
|
106
|
-
label: RegExp;
|
|
107
|
-
validValue: string;
|
|
108
|
-
interaction: FieldInteraction;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Maps a detected field (by its label) to the valid test value used
|
|
112
|
-
* to fill it. Presence here does NOT imply the field is required —
|
|
113
|
-
* required-ness is determined dynamically per field via
|
|
114
|
-
* `isFieldRequired`. Email is the only field that is always required.
|
|
115
|
-
*/
|
|
116
|
-
export declare const FieldValueMappings: FieldValueMapping[];
|
|
117
|
-
export declare function fillDateTimePicker(locator: Locator, fallbackValue: string): Promise<void>;
|
|
118
|
-
export declare function fillFormFields(page: Page): Promise<void>;
|
|
119
|
-
export declare function hasVisibleErrorNearField(fieldLocator: Locator): Promise<boolean>;
|
|
120
|
-
export interface InvalidInputCase {
|
|
121
|
-
fieldLabel: RegExp;
|
|
122
|
-
invalidValue: string;
|
|
123
|
-
expectedErrorText: RegExp;
|
|
124
|
-
}
|
|
125
|
-
export declare const InvalidInputCases: InvalidInputCase[];
|
|
126
|
-
export declare function fillOtherRequiredFieldsValidly(page: Page, skipLabel: RegExp): Promise<void>;
|
|
127
|
-
export interface AutofillField {
|
|
128
|
-
label: RegExp;
|
|
129
|
-
expectedAutocomplete: RegExp;
|
|
130
|
-
}
|
|
131
|
-
export declare const AutofillFields: AutofillField[];
|
|
132
|
-
export interface KeyboardField {
|
|
133
|
-
label: RegExp;
|
|
134
|
-
expectedInputMode: RegExp;
|
|
135
|
-
}
|
|
136
|
-
export declare const KeyboardFields: KeyboardField[];
|
|
137
|
-
export declare function getActiveSlideIndex(carousel: Locator): Promise<number>;
|
|
138
|
-
export declare function getTotalSlideCount(carousel: Locator): Promise<number>;
|
|
139
|
-
export declare function hasEmptySlideFrame(carousel: Locator): Promise<boolean>;
|
|
140
|
-
export declare function goToNextSlideViaArrow(carousel: Locator): Promise<boolean>;
|
|
141
|
-
export declare function getDotCount(carousel: Locator): Promise<number>;
|
|
142
|
-
export declare function goToSlideViaDot(carousel: Locator, dotIndex: number): Promise<boolean>;
|
|
143
|
-
export declare function swipeToNextSlide(carousel: Locator): Promise<boolean>;
|
|
144
|
-
export declare function swipeToPreviousSlide(carousel: Locator): Promise<boolean>;
|
|
145
|
-
export declare function traverseSlidesForward(carousel: Locator, carouselIndex: number, isMobile: boolean): Promise<void>;
|
|
146
|
-
export declare function checkSwipeBackward(carousel: Locator, carouselIndex: number): Promise<void>;
|
|
147
|
-
export declare function checkAllDots(carousel: Locator, carouselIndex: number): Promise<void>;
|
|
148
|
-
export declare const SafeAreaZones: {
|
|
149
|
-
readonly Top: "top";
|
|
150
|
-
readonly Bottom: "bottom";
|
|
151
|
-
readonly Left: "left";
|
|
152
|
-
readonly Right: "right";
|
|
153
|
-
};
|
|
154
|
-
export type SafeAreaZone = (typeof SafeAreaZones)[keyof typeof SafeAreaZones];
|
|
155
|
-
export interface SafeAreaOverlap extends ElementDescriptor {
|
|
156
|
-
text: string;
|
|
157
|
-
overlapZone: SafeAreaZone;
|
|
158
|
-
}
|
|
159
|
-
export declare function findSafeAreaOverlaps(page: Page): Promise<SafeAreaOverlap[]>;
|
|
160
3
|
interface FillFieldParams {
|
|
161
4
|
page: Page;
|
|
162
5
|
locator: Locator;
|
|
@@ -168,4 +11,7 @@ export declare function fillField({ field, locator, page }: FillFieldParams): Pr
|
|
|
168
11
|
* Native validation blocks the submit itself, so no custom error is rendered for it.
|
|
169
12
|
*/
|
|
170
13
|
export declare function hasNativeValidationError(fieldLocator: Locator): Promise<boolean>;
|
|
14
|
+
export declare function toSnapshotName(targetPage: string): string;
|
|
15
|
+
export declare function hasHorizontalScrollbar(page: Page): Promise<boolean>;
|
|
16
|
+
export declare function scrollThroughPage(page: Page): Promise<void>;
|
|
171
17
|
export {};
|