@manufac/general-tests 0.3.0 → 0.4.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 +69 -59
- package/dist/config/projects.d.ts +2 -6
- package/dist/config/projects.js +3 -3
- package/dist/fixtures/index.d.ts +2 -0
- package/dist/fixtures/index.js +1 -0
- package/dist/schema/index.d.ts +33 -26
- package/dist/schema/index.js +8 -36
- package/dist/specs/forms.spec.d.ts +1 -0
- package/dist/specs/forms.spec.js +74 -0
- package/dist/specs/responsive-layout.spec.js +1 -1
- package/dist/specs/utils.d.ts +13 -1
- package/dist/specs/utils.js +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,74 +9,84 @@ See [CHANGELOG.md](./CHANGELOG.md) for release notes.
|
|
|
9
9
|
Add it as a normal dependency, pinned to the version you want to consume:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install @manufac/general-tests
|
|
12
|
+
npm install @manufac/general-tests
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
Then wire it into the app's `playwright.config.ts`.
|
|
16
|
+
|
|
17
|
+
First, declare the app-specific pages in a small local config file, typed against `ManufacTestConfig`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// manufac-test.config.ts
|
|
21
|
+
import type { ManufacTestConfig } from "@manufac/general-tests/schema/index.js";
|
|
22
|
+
|
|
23
|
+
export const Config: ManufacTestConfig = {
|
|
24
|
+
pages: ["/", "/pricing", "/contact-us", "/privacy"],
|
|
25
|
+
};
|
|
21
26
|
```
|
|
22
27
|
|
|
23
|
-
Then
|
|
28
|
+
Then reference it from `playwright.config.ts`:
|
|
24
29
|
|
|
25
30
|
```ts
|
|
26
|
-
import { defineConfig } from "@playwright/test";
|
|
27
|
-
import {
|
|
31
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import {
|
|
34
|
+
DesktopBreakpoints,
|
|
35
|
+
ResponsiveLayoutBreakpoints,
|
|
36
|
+
LoadingPageTestMatch,
|
|
37
|
+
ResponsiveLayoutTestMatch,
|
|
38
|
+
} from "@manufac/general-tests/config/projects.js";
|
|
39
|
+
import type { PageTestOptions } from "@manufac/general-tests/fixtures/index.js";
|
|
40
|
+
import type { Project } from "@playwright/test";
|
|
41
|
+
import { Config } from "./manufac-test.config.js";
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
// Resolves to node_modules/@manufac/general-tests/dist/specs, wherever it's installed from.
|
|
44
|
+
const GeneralTestsSpecsDir = join(dirname(require.resolve("@manufac/general-tests/config/projects.js")), "..", "specs");
|
|
45
|
+
|
|
46
|
+
const Projects: Project[] = [
|
|
47
|
+
{
|
|
48
|
+
name: "Desktop Chrome",
|
|
49
|
+
testDir: GeneralTestsSpecsDir,
|
|
50
|
+
testMatch: [LoadingPageTestMatch, ResponsiveLayoutTestMatch],
|
|
51
|
+
use: { ...devices["Desktop Chrome"], viewport: DesktopBreakpoints.Desktop },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "Desktop Safari",
|
|
55
|
+
testDir: GeneralTestsSpecsDir,
|
|
56
|
+
testMatch: [LoadingPageTestMatch, ResponsiveLayoutTestMatch],
|
|
57
|
+
use: { ...devices["Desktop Safari"], viewport: DesktopBreakpoints.Desktop },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Desktop Firefox",
|
|
61
|
+
testDir: GeneralTestsSpecsDir,
|
|
62
|
+
testMatch: [LoadingPageTestMatch, ResponsiveLayoutTestMatch],
|
|
63
|
+
use: { ...devices["Desktop Firefox"], viewport: DesktopBreakpoints.Desktop },
|
|
32
64
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}),
|
|
39
|
-
createProject({
|
|
40
|
-
name: "Desktop Safari",
|
|
41
|
-
device: "Desktop Safari",
|
|
42
|
-
configuration: { projectType: "desktop", breakpoint: Breakpoints.Desktop },
|
|
43
|
-
}),
|
|
44
|
-
createProject({
|
|
45
|
-
name: "Desktop Firefox",
|
|
46
|
-
device: "Desktop Firefox",
|
|
47
|
-
configuration: { projectType: "desktop", breakpoint: Breakpoints.Desktop },
|
|
48
|
-
}),
|
|
49
|
-
createProject({
|
|
50
|
-
name: "Mobile Safari",
|
|
51
|
-
device: "iPhone 12",
|
|
52
|
-
configuration: { projectType: "mobile", breakpoint: Breakpoints.MobileM },
|
|
53
|
-
}),
|
|
54
|
-
createProject({
|
|
55
|
-
name: "Mobile Chrome",
|
|
56
|
-
device: "Pixel 5",
|
|
57
|
-
configuration: { projectType: "mobile", breakpoint: Breakpoints.MobileM },
|
|
58
|
-
}),
|
|
59
|
-
],
|
|
60
|
-
webServer: {
|
|
61
|
-
command: "npm run start",
|
|
62
|
-
url: "http://localhost:8080",
|
|
63
|
-
reuseExistingServer: !process.env.CI,
|
|
65
|
+
{
|
|
66
|
+
name: "Responsive 375",
|
|
67
|
+
testDir: GeneralTestsSpecsDir,
|
|
68
|
+
testMatch: ResponsiveLayoutTestMatch,
|
|
69
|
+
use: { ...devices["Desktop Chrome"], viewport: ResponsiveLayoutBreakpoints.MobileL },
|
|
64
70
|
},
|
|
71
|
+
{
|
|
72
|
+
name: "Responsive 768",
|
|
73
|
+
testDir: GeneralTestsSpecsDir,
|
|
74
|
+
testMatch: ResponsiveLayoutTestMatch,
|
|
75
|
+
use: { ...devices["Desktop Chrome"], viewport: ResponsiveLayoutBreakpoints.Tablet },
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export default defineConfig<PageTestOptions>({
|
|
80
|
+
testDir: "./e2e",
|
|
81
|
+
use: {
|
|
82
|
+
baseURL: "https://your-app.example.com",
|
|
83
|
+
pages: Config.pages,
|
|
84
|
+
},
|
|
85
|
+
projects: Projects,
|
|
65
86
|
});
|
|
66
87
|
```
|
|
67
88
|
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
70
|
-
- `
|
|
71
|
-
|
|
72
|
-
By default every spec runs against `/`. To point a project at a different page, override the `targetPage` fixture option:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
createProject({
|
|
76
|
-
name: "Desktop Chrome — pricing",
|
|
77
|
-
device: "Desktop Chrome",
|
|
78
|
-
configuration: { projectType: "desktop", breakpoint: Breakpoints.Desktop },
|
|
79
|
-
}),
|
|
80
|
-
// then, in that project's `use`:
|
|
81
|
-
use: { targetPage: "/pricing" }
|
|
82
|
-
```
|
|
89
|
+
- `LoadingPageTestMatch` runs `loading-page.spec.ts` — page-load/console-error/network checks, once per project, over every page in `Config.pages`.
|
|
90
|
+
- `ResponsiveLayoutTestMatch` runs `responsive-layout.spec.ts` — checked at whichever `viewport` each project sets, so add one project per breakpoint you care about (`ResponsiveLayoutBreakpoints.MobileL` / `.Tablet` / `.Desktop`).
|
|
91
|
+
- `require.resolve` + `join(..., "..", "specs")` finds the package's built `specs` directory regardless of how it's installed (registry, `file:`, or a vendored tarball) — `testDir` must point there since these two spec files aren't re-exported any other way.
|
|
92
|
+
- `PageTestOptions` (from `fixtures/index.js`) types the `use` block so `pages` (and `forms`, if you set it) are checked against the fixture's actual shape.
|
|
@@ -11,11 +11,6 @@ export declare const DesktopBreakpoints: {
|
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
export declare const ResponsiveLayoutBreakpoints: {
|
|
14
|
-
readonly MobileS: {
|
|
15
|
-
readonly name: "320";
|
|
16
|
-
readonly width: 320;
|
|
17
|
-
readonly height: 568;
|
|
18
|
-
};
|
|
19
14
|
readonly MobileL: {
|
|
20
15
|
readonly name: "375";
|
|
21
16
|
readonly width: 375;
|
|
@@ -37,5 +32,6 @@ export declare const ProjectTypes: {
|
|
|
37
32
|
readonly Desktop: "desktop";
|
|
38
33
|
readonly Shared: "shared";
|
|
39
34
|
};
|
|
40
|
-
export declare const LoadingPageTestMatch = "loading-page.spec.
|
|
35
|
+
export declare const LoadingPageTestMatch = "loading-page.spec.js";
|
|
41
36
|
export declare const ResponsiveLayoutTestMatch = "responsive-layout.spec.js";
|
|
37
|
+
export declare const FormsTestMatch = "forms.spec.js";
|
package/dist/config/projects.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ResponsiveLayoutTestMatch = exports.LoadingPageTestMatch = exports.ProjectTypes = exports.ResponsiveLayoutBreakpoints = exports.DesktopBreakpoints = void 0;
|
|
3
|
+
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 },
|
|
7
7
|
};
|
|
8
8
|
exports.ResponsiveLayoutBreakpoints = {
|
|
9
|
-
MobileS: { name: "320", width: 320, height: 568 },
|
|
10
9
|
MobileL: { name: "375", width: 375, height: 812 },
|
|
11
10
|
Tablet: { name: "768", width: 768, height: 1024 },
|
|
12
11
|
Desktop: { name: "1440", width: 1440, height: 900 },
|
|
@@ -16,5 +15,6 @@ exports.ProjectTypes = {
|
|
|
16
15
|
Desktop: "desktop",
|
|
17
16
|
Shared: "shared",
|
|
18
17
|
};
|
|
19
|
-
exports.LoadingPageTestMatch = "loading-page.spec.
|
|
18
|
+
exports.LoadingPageTestMatch = "loading-page.spec.js";
|
|
20
19
|
exports.ResponsiveLayoutTestMatch = "responsive-layout.spec.js";
|
|
20
|
+
exports.FormsTestMatch = "forms.spec.js";
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { ManufacTestFormConfig } from "../schema";
|
|
1
2
|
export interface PageTestOptions {
|
|
2
3
|
targetPage: string;
|
|
3
4
|
pages: string[];
|
|
5
|
+
forms: ManufacTestFormConfig[];
|
|
4
6
|
consoleErrors: string[];
|
|
5
7
|
}
|
|
6
8
|
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
|
@@ -8,6 +8,7 @@ exports.test = test_1.test.extend({
|
|
|
8
8
|
// Ref: https://playwright.dev/docs/test-fixtures#fixtures-options
|
|
9
9
|
targetPage: ["/", { option: true }],
|
|
10
10
|
pages: [["/"], { option: true }],
|
|
11
|
+
forms: [[], { option: true }],
|
|
11
12
|
consoleErrors: async ({ page }, use) => {
|
|
12
13
|
const errors = [];
|
|
13
14
|
page.on("console", (message) => {
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type FieldInteraction = (typeof FieldInteractions)[keyof typeof FieldInte
|
|
|
13
13
|
* (e.g. a field with no `invalidValue` is simply skipped by the invalid-input check for that form).
|
|
14
14
|
*/
|
|
15
15
|
declare const FormFieldSchema: z.ZodObject<{
|
|
16
|
-
|
|
16
|
+
fieldID: z.ZodString;
|
|
17
17
|
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
18
|
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
19
19
|
/** A plain input */
|
|
@@ -22,6 +22,7 @@ declare const FormFieldSchema: z.ZodObject<{
|
|
|
22
22
|
readonly Datetime: "datetime";
|
|
23
23
|
}>>>;
|
|
24
24
|
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
|
+
isRequired: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
25
26
|
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
26
27
|
expectedInputMode: z.ZodOptional<z.ZodNullable<z.ZodCustom<"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined, "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined>>>;
|
|
27
28
|
}, z.core.$strip>;
|
|
@@ -30,9 +31,10 @@ export type ManufacTestFormField = z.infer<typeof FormFieldSchema>;
|
|
|
30
31
|
* Represents One form to be tested
|
|
31
32
|
*/
|
|
32
33
|
declare const FormSchema: z.ZodObject<{
|
|
33
|
-
|
|
34
|
+
formID: z.ZodString;
|
|
35
|
+
page: z.ZodString;
|
|
34
36
|
fields: z.ZodArray<z.ZodObject<{
|
|
35
|
-
|
|
37
|
+
fieldID: z.ZodString;
|
|
36
38
|
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
39
|
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
38
40
|
/** A plain input */
|
|
@@ -41,37 +43,40 @@ declare const FormSchema: z.ZodObject<{
|
|
|
41
43
|
readonly Datetime: "datetime";
|
|
42
44
|
}>>>;
|
|
43
45
|
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
|
+
isRequired: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
44
47
|
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
45
48
|
expectedInputMode: z.ZodOptional<z.ZodNullable<z.ZodCustom<"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined, "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined>>>;
|
|
46
49
|
}, z.core.$strip>>;
|
|
50
|
+
successMessage: z.ZodString;
|
|
47
51
|
}, z.core.$strip>;
|
|
48
|
-
export type
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
export type ManufacTestFormConfig = z.infer<typeof FormSchema>;
|
|
53
|
+
/** Validates the `forms` fixture option. */
|
|
54
|
+
export declare const ManufacTestFormsSchema: z.ZodArray<z.ZodObject<{
|
|
55
|
+
formID: z.ZodString;
|
|
56
|
+
page: z.ZodString;
|
|
57
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
58
|
+
fieldID: z.ZodString;
|
|
59
|
+
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
60
|
+
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
61
|
+
/** A plain input */
|
|
62
|
+
readonly Fill: "fill";
|
|
63
|
+
/** A popup-based date/time picker widget */
|
|
64
|
+
readonly Datetime: "datetime";
|
|
65
|
+
}>>>;
|
|
66
|
+
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
67
|
+
isRequired: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
68
|
+
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
69
|
+
expectedInputMode: z.ZodOptional<z.ZodNullable<z.ZodCustom<"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined, "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined>>>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
successMessage: z.ZodString;
|
|
72
|
+
}, z.core.$strip>>;
|
|
61
73
|
export declare const ManufacTestConfigSchema: z.ZodObject<{
|
|
62
74
|
pages: z.ZodArray<z.ZodString>;
|
|
63
|
-
selectors: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
64
|
-
hamburgerTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
65
|
-
ctaTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
66
|
-
cookieAcceptTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
67
|
-
homeLinkTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
68
|
-
carouselSlideTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
69
|
-
carouselDotTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
70
|
-
}, z.core.$strip>>>;
|
|
71
75
|
forms: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
72
|
-
|
|
76
|
+
formID: z.ZodString;
|
|
77
|
+
page: z.ZodString;
|
|
73
78
|
fields: z.ZodArray<z.ZodObject<{
|
|
74
|
-
|
|
79
|
+
fieldID: z.ZodString;
|
|
75
80
|
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
76
81
|
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
77
82
|
/** A plain input */
|
|
@@ -80,9 +85,11 @@ export declare const ManufacTestConfigSchema: z.ZodObject<{
|
|
|
80
85
|
readonly Datetime: "datetime";
|
|
81
86
|
}>>>;
|
|
82
87
|
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
88
|
+
isRequired: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
83
89
|
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
84
90
|
expectedInputMode: z.ZodOptional<z.ZodNullable<z.ZodCustom<"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined, "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined>>>;
|
|
85
91
|
}, z.core.$strip>>;
|
|
92
|
+
successMessage: z.ZodString;
|
|
86
93
|
}, z.core.$strip>>>>;
|
|
87
94
|
}, z.core.$strip>;
|
|
88
95
|
export type ManufacTestConfig = z.infer<typeof ManufacTestConfigSchema>;
|
package/dist/schema/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ManufacTestConfigSchema = exports.FieldInteractions = void 0;
|
|
3
|
+
exports.ManufacTestConfigSchema = exports.ManufacTestFormsSchema = exports.FieldInteractions = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
/** Interaction kind for filling a form field. */
|
|
6
6
|
exports.FieldInteractions = {
|
|
@@ -15,54 +15,26 @@ exports.FieldInteractions = {
|
|
|
15
15
|
* (e.g. a field with no `invalidValue` is simply skipped by the invalid-input check for that form).
|
|
16
16
|
*/
|
|
17
17
|
const FormFieldSchema = zod_1.z.object({
|
|
18
|
-
|
|
19
|
-
testID: zod_1.z.string().min(1),
|
|
20
|
-
/** Value used to fill this field on the happy path. Omit to skip filling this field. */
|
|
18
|
+
fieldID: zod_1.z.string().min(1),
|
|
21
19
|
validValue: zod_1.z.string().nullish(),
|
|
22
|
-
/** How to interact with (or fill) this input */
|
|
23
20
|
interaction: zod_1.z.enum(exports.FieldInteractions).nullish(),
|
|
24
|
-
/**
|
|
25
|
-
* Deliberately invalid value used by the invalid-input check. Omit to skip that check for this field.
|
|
26
|
-
* - The check only verifies the field is left in an invalid state (`aria-invalid` or a failing native `checkValidity()`).
|
|
27
|
-
*/
|
|
28
21
|
invalidValue: zod_1.z.string().nullish(),
|
|
29
|
-
|
|
30
|
-
/** Expected `autocomplete` attribute value, checked by the autofill spec. Omit to skip that check. */
|
|
22
|
+
isRequired: zod_1.z.boolean().nullish(),
|
|
31
23
|
expectedAutocomplete: zod_1.z.custom().nullish(),
|
|
32
|
-
/** Expected `inputmode` attribute value, checked by the soft-keyboard spec. Omit to skip that check. */
|
|
33
24
|
expectedInputMode: zod_1.z.custom().nullish(),
|
|
34
25
|
});
|
|
35
26
|
/**
|
|
36
27
|
* Represents One form to be tested
|
|
37
28
|
*/
|
|
38
29
|
const FormSchema = zod_1.z.object({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Fields belonging to this form. */
|
|
30
|
+
formID: zod_1.z.string().min(1),
|
|
31
|
+
page: zod_1.z.string().min(1),
|
|
42
32
|
fields: zod_1.z.array(FormFieldSchema).min(1),
|
|
33
|
+
successMessage: zod_1.z.string().min(1),
|
|
43
34
|
});
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
const ManufacTestSelectorsSchema = zod_1.z.object({
|
|
48
|
-
/** Mobile nav hamburger/menu-toggle button. Single element — this testid must be unique on the page. */
|
|
49
|
-
hamburgerTestID: zod_1.z.string().min(1).nullish(),
|
|
50
|
-
/** Primary CTA button/link checked by CT-01. Single element — this testid must be unique on the page. */
|
|
51
|
-
ctaTestID: zod_1.z.string().min(1).nullish(),
|
|
52
|
-
/** Cookie-consent banner's accept/dismiss button. Single element — this testid must be unique on the page. */
|
|
53
|
-
cookieAcceptTestID: zod_1.z.string().min(1).nullish(),
|
|
54
|
-
/** "Back to home" link shown on the 404 page. Single element — this testid must be unique on the page. */
|
|
55
|
-
homeLinkTestID: zod_1.z.string().min(1).nullish(),
|
|
56
|
-
/** Shared testID present on every carousel slide — a group, not one element: put the SAME testid on every slide. */
|
|
57
|
-
carouselSlideTestID: zod_1.z.string().min(1).nullish(),
|
|
58
|
-
/** Shared testID present on every carousel pagination dot — a group, not one element: put the SAME testid on every dot. */
|
|
59
|
-
carouselDotTestID: zod_1.z.string().min(1).nullish(),
|
|
60
|
-
});
|
|
35
|
+
/** Validates the `forms` fixture option. */
|
|
36
|
+
exports.ManufacTestFormsSchema = zod_1.z.array(FormSchema);
|
|
61
37
|
exports.ManufacTestConfigSchema = zod_1.z.object({
|
|
62
|
-
/** Pages this suite should run against. */
|
|
63
38
|
pages: zod_1.z.array(zod_1.z.string()).min(1, "At least one page is required"),
|
|
64
|
-
/** Defines selectors for commonly tested components. */
|
|
65
|
-
selectors: ManufacTestSelectorsSchema.nullish(),
|
|
66
|
-
/** Forms this suite should exercise, keyed by their own data-testid. Omit to skip form-related checks. */
|
|
67
39
|
forms: zod_1.z.array(FormSchema).nullish(),
|
|
68
40
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
fixtures_1.test.describe("Forms & lead capture", () => {
|
|
7
|
+
(0, fixtures_1.test)("FM-01 Happy path submission", async ({ page, forms, consoleErrors }) => {
|
|
8
|
+
fixtures_1.test.slow();
|
|
9
|
+
for (const form of forms) {
|
|
10
|
+
await fixtures_1.test.step(`${form.page} (${form.formID})`, async () => {
|
|
11
|
+
const consoleErrorsBeforeSubmit = consoleErrors.length; // Capture errors while filling the forms.
|
|
12
|
+
await page.goto(form.page);
|
|
13
|
+
const formLocator = page.getByTestId(form.formID);
|
|
14
|
+
await (0, test_1.expect)(formLocator).toBeVisible();
|
|
15
|
+
for (const field of form.fields) {
|
|
16
|
+
await (0, utils_1.fillField)({ page, locator: formLocator, field });
|
|
17
|
+
}
|
|
18
|
+
const submittedAt = new Date().toISOString();
|
|
19
|
+
await formLocator.locator('button[type="submit"]').click();
|
|
20
|
+
await (0, test_1.expect)(page.getByText(form.successMessage, {
|
|
21
|
+
exact: true,
|
|
22
|
+
})).toBeVisible();
|
|
23
|
+
(0, test_1.expect)(consoleErrors.slice(consoleErrorsBeforeSubmit)).toHaveLength(0);
|
|
24
|
+
// Manual, staging-only follow-up — see downstream check below.
|
|
25
|
+
fixtures_1.test.info().annotations.push({
|
|
26
|
+
type: "FM-01 downstream check",
|
|
27
|
+
description: `Verify "${form.formID}" submission from ${form.page} on staging (submitted ${submittedAt}).`,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
(0, fixtures_1.test)("FM-02 Required field validation", async ({ page, forms }) => {
|
|
33
|
+
fixtures_1.test.slow();
|
|
34
|
+
for (const form of forms) {
|
|
35
|
+
const requiredFields = form.fields.filter((field) => {
|
|
36
|
+
return field.isRequired === true;
|
|
37
|
+
});
|
|
38
|
+
for (const requiredField of requiredFields) {
|
|
39
|
+
await fixtures_1.test.step(`${form.page} (${form.formID}) - ${requiredField.fieldID}`, async () => {
|
|
40
|
+
await page.goto(form.page);
|
|
41
|
+
const formLocator = page.getByTestId(form.formID);
|
|
42
|
+
await (0, test_1.expect)(formLocator).toBeVisible();
|
|
43
|
+
for (const field of form.fields) {
|
|
44
|
+
await (0, utils_1.fillField)({
|
|
45
|
+
page,
|
|
46
|
+
locator: formLocator,
|
|
47
|
+
field: {
|
|
48
|
+
...field,
|
|
49
|
+
validValue: field.fieldID === requiredField.fieldID ? "" : field.validValue,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
await formLocator.locator('button[type="submit"]').click();
|
|
54
|
+
await (0, test_1.expect)(page.getByText(new RegExp(form.successMessage, "i"))).not.toBeVisible();
|
|
55
|
+
const requiredFieldLocator = formLocator.getByTestId(requiredField.fieldID);
|
|
56
|
+
const hasNativeError = await (0, utils_1.hasNativeValidationError)(requiredFieldLocator); // Checks for the browser native validation error for required field.
|
|
57
|
+
if (hasNativeError) {
|
|
58
|
+
(0, test_1.expect)(hasNativeError).toBeTruthy();
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await (0, test_1.expect)(requiredFieldLocator).toHaveAttribute("aria-invalid", "true");
|
|
62
|
+
const describedBy = await requiredFieldLocator.getAttribute("aria-describedby");
|
|
63
|
+
(0, test_1.expect)(describedBy, `"${requiredField.fieldID}" should be linked to its error via aria-describedby`).toBeTruthy();
|
|
64
|
+
const errorID = describedBy?.split(" ").at(0) ?? ""; // aria-describedby can hold several element IDs, separated by spaces Ref: https://github.com/mantinedev/mantine/blob/e49d1d94280d9841ff27a46f1935ebcba0f1b639/packages/%40mantine/core/src/components/Input/InputWrapper/InputWrapper.tsx#L213
|
|
65
|
+
const errorLocator = page.locator(`#${errorID}`);
|
|
66
|
+
await (0, test_1.expect)(errorLocator).toBeVisible();
|
|
67
|
+
const errorText = (await errorLocator.textContent())?.trim() ?? "";
|
|
68
|
+
(0, test_1.expect)(errorText, `"${requiredField.fieldID}" error message should mention it's required`).toMatch(/required/i);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -4,7 +4,7 @@ const utils_js_1 = require("./utils.js");
|
|
|
4
4
|
const fixtures_1 = require("../fixtures");
|
|
5
5
|
const test_1 = require("@playwright/test");
|
|
6
6
|
fixtures_1.test.describe("Responsive layout", () => {
|
|
7
|
-
(0, fixtures_1.test)("RS-01: No horizontal overflow at
|
|
7
|
+
(0, fixtures_1.test)("RS-01: No horizontal overflow at 375 / 768 / 1440", async ({ page, pages }) => {
|
|
8
8
|
fixtures_1.test.slow();
|
|
9
9
|
for (const targetPage of pages) {
|
|
10
10
|
await fixtures_1.test.step(targetPage, async () => {
|
package/dist/specs/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Locator, type Page } from "@playwright/test";
|
|
2
|
+
import type { ManufacTestFormField } from "../schema";
|
|
2
3
|
export declare const ResponsiveConfig: {
|
|
3
4
|
readonly ScrollSteps: 10;
|
|
4
5
|
readonly ScrollStepDelayMs: 100;
|
|
@@ -156,4 +157,15 @@ export interface SafeAreaOverlap extends ElementDescriptor {
|
|
|
156
157
|
overlapZone: SafeAreaZone;
|
|
157
158
|
}
|
|
158
159
|
export declare function findSafeAreaOverlaps(page: Page): Promise<SafeAreaOverlap[]>;
|
|
160
|
+
interface FillFieldParams {
|
|
161
|
+
page: Page;
|
|
162
|
+
locator: Locator;
|
|
163
|
+
field: ManufacTestFormField;
|
|
164
|
+
}
|
|
165
|
+
export declare function fillField({ field, locator, page }: FillFieldParams): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Checks whether the browser's native validation (e.g. the `required` attribute) rejects the field's current value.
|
|
168
|
+
* Native validation blocks the submit itself, so no custom error is rendered for it.
|
|
169
|
+
*/
|
|
170
|
+
export declare function hasNativeValidationError(fieldLocator: Locator): Promise<boolean>;
|
|
159
171
|
export {};
|
package/dist/specs/utils.js
CHANGED
|
@@ -33,6 +33,8 @@ exports.traverseSlidesForward = traverseSlidesForward;
|
|
|
33
33
|
exports.checkSwipeBackward = checkSwipeBackward;
|
|
34
34
|
exports.checkAllDots = checkAllDots;
|
|
35
35
|
exports.findSafeAreaOverlaps = findSafeAreaOverlaps;
|
|
36
|
+
exports.fillField = fillField;
|
|
37
|
+
exports.hasNativeValidationError = hasNativeValidationError;
|
|
36
38
|
const test_1 = require("@playwright/test");
|
|
37
39
|
exports.ResponsiveConfig = {
|
|
38
40
|
ScrollSteps: 10,
|
|
@@ -856,3 +858,58 @@ async function findSafeAreaOverlaps(page) {
|
|
|
856
858
|
return overlaps;
|
|
857
859
|
}, { selector: contentSelector });
|
|
858
860
|
}
|
|
861
|
+
const MonthNames = [
|
|
862
|
+
"January",
|
|
863
|
+
"February",
|
|
864
|
+
"March",
|
|
865
|
+
"April",
|
|
866
|
+
"May",
|
|
867
|
+
"June",
|
|
868
|
+
"July",
|
|
869
|
+
"August",
|
|
870
|
+
"September",
|
|
871
|
+
"October",
|
|
872
|
+
"November",
|
|
873
|
+
"December",
|
|
874
|
+
];
|
|
875
|
+
async function fillDatetime({ field, locator, page }) {
|
|
876
|
+
// A fresh page load already leaves the picker empty; nothing to clear or open for.
|
|
877
|
+
if (field.validValue != null && field.validValue !== "") {
|
|
878
|
+
const date = new Date(field.validValue);
|
|
879
|
+
await locator.click();
|
|
880
|
+
const dialog = page.getByRole("dialog");
|
|
881
|
+
await (0, test_1.expect)(dialog).toBeVisible(); // Check dialog for visibility to select months
|
|
882
|
+
const dayLabel = `${date.getDate().toString()} ${MonthNames[date.getMonth()]} ${date.getFullYear().toString()}`;
|
|
883
|
+
await dialog.getByRole("button", { name: dayLabel, exact: true }).click(); // Get the exact date cell with aria-label comparison.
|
|
884
|
+
const hour24 = date.getHours();
|
|
885
|
+
const hour12 = ((hour24 + 11) % 12) + 1;
|
|
886
|
+
const period = hour24 >= 12 ? "PM" : "AM";
|
|
887
|
+
const timeSegments = dialog.getByRole("spinbutton");
|
|
888
|
+
await timeSegments.nth(0).fill(hour12.toString().padStart(2, "0")); // First spin button is hour scroll
|
|
889
|
+
await timeSegments.nth(1).fill(date.getMinutes().toString().padStart(2, "0")); // second spin button is minute scroll
|
|
890
|
+
await dialog.getByRole("textbox").fill(period);
|
|
891
|
+
await dialog.getByRole("button").last().click(); // No specific selectors available for this button like aria-* attributes so using .last as selector
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function fillField({ field, locator, page }) {
|
|
895
|
+
const fieldLocator = locator.getByTestId(field.fieldID);
|
|
896
|
+
if (field.interaction === exports.FieldInteractions.Datetime) {
|
|
897
|
+
await fillDatetime({
|
|
898
|
+
field,
|
|
899
|
+
locator: fieldLocator,
|
|
900
|
+
page,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
else if (field.validValue != null) {
|
|
904
|
+
await fieldLocator.fill(field.validValue);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Checks whether the browser's native validation (e.g. the `required` attribute) rejects the field's current value.
|
|
909
|
+
* Native validation blocks the submit itself, so no custom error is rendered for it.
|
|
910
|
+
*/
|
|
911
|
+
async function hasNativeValidationError(fieldLocator) {
|
|
912
|
+
return await fieldLocator.evaluate((element) => {
|
|
913
|
+
return element instanceof HTMLInputElement && !element.checkValidity();
|
|
914
|
+
});
|
|
915
|
+
}
|