@manufac/general-tests 0.1.0 → 0.2.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/dist/config/projects.d.ts +24 -35
- package/dist/config/projects.js +13 -28
- package/dist/schema/index.d.ts +84 -0
- package/dist/schema/index.js +62 -1
- package/dist/specs/shared/forms/input-edge.spec.js +8 -4
- package/dist/specs/shared/forms/invalid-input.spec.js +1 -3
- package/dist/specs/shared/forms/required-field.spec.js +11 -4
- package/dist/specs/shared/forms/thankyou-confirm.spec.js +13 -3
- package/dist/specs/shared/landing-page.spec.js +5 -127
- package/dist/specs/site-wide/landing-page.spec.d.ts +1 -0
- package/dist/specs/site-wide/landing-page.spec.js +38 -0
- package/dist/specs/utils.js +48 -13
- package/package.json +3 -2
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
import { devices } from "@playwright/test";
|
|
2
|
-
import type { Project } from "@playwright/test";
|
|
3
1
|
export interface Breakpoint {
|
|
4
2
|
name: string;
|
|
5
3
|
width: number;
|
|
6
4
|
height: number;
|
|
7
5
|
}
|
|
8
|
-
export declare const
|
|
9
|
-
readonly MobileS: {
|
|
10
|
-
readonly name: "Mobile S";
|
|
11
|
-
readonly width: 320;
|
|
12
|
-
readonly height: 568;
|
|
13
|
-
};
|
|
14
|
-
readonly MobileM: {
|
|
15
|
-
readonly name: "Mobile M";
|
|
16
|
-
readonly width: 390;
|
|
17
|
-
readonly height: 844;
|
|
18
|
-
};
|
|
19
|
-
readonly Tablet: {
|
|
20
|
-
readonly name: "Tablet";
|
|
21
|
-
readonly width: 768;
|
|
22
|
-
readonly height: 1024;
|
|
23
|
-
};
|
|
6
|
+
export declare const DesktopBreakpoints: {
|
|
24
7
|
readonly Laptop: {
|
|
25
8
|
readonly name: "Laptop";
|
|
26
9
|
readonly width: 1280;
|
|
@@ -37,23 +20,29 @@ export declare const Breakpoints: {
|
|
|
37
20
|
readonly height: 1080;
|
|
38
21
|
};
|
|
39
22
|
};
|
|
40
|
-
declare const
|
|
23
|
+
export declare const MobileBreakpoints: {
|
|
24
|
+
readonly MobileS: {
|
|
25
|
+
readonly name: "Mobile S";
|
|
26
|
+
readonly width: 320;
|
|
27
|
+
readonly height: 568;
|
|
28
|
+
};
|
|
29
|
+
readonly MobileM: {
|
|
30
|
+
readonly name: "Mobile M";
|
|
31
|
+
readonly width: 390;
|
|
32
|
+
readonly height: 844;
|
|
33
|
+
};
|
|
34
|
+
readonly Tablet: {
|
|
35
|
+
readonly name: "Tablet";
|
|
36
|
+
readonly width: 768;
|
|
37
|
+
readonly height: 1024;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export declare const ProjectTypes: {
|
|
41
41
|
readonly Mobile: "mobile";
|
|
42
42
|
readonly Desktop: "desktop";
|
|
43
|
+
readonly Shared: "shared";
|
|
43
44
|
};
|
|
44
|
-
export
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
export interface DesktopConfig {
|
|
50
|
-
projectType: typeof ProjectTypes.Desktop;
|
|
51
|
-
breakpoint: typeof Breakpoints.Desktop;
|
|
52
|
-
}
|
|
53
|
-
export interface CreateProjectParams {
|
|
54
|
-
name: string;
|
|
55
|
-
device: keyof typeof devices;
|
|
56
|
-
configuration: MobileConfig | DesktopConfig;
|
|
57
|
-
}
|
|
58
|
-
export declare function createProject({ configuration, device, name }: CreateProjectParams): Project;
|
|
59
|
-
export {};
|
|
45
|
+
export declare const SharedTestMatch = "shared/**/*.spec.js";
|
|
46
|
+
export declare const DesktopOnlyTestMatch = "desktop-only/**/*.spec.js";
|
|
47
|
+
export declare const MobileOnlyTestMatch = "mobile-only/**/*.spec.js";
|
|
48
|
+
export declare const SiteWideTestMatch = "site-wide/**/*.spec.js";
|
package/dist/config/projects.js
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.createProject = createProject;
|
|
5
|
-
const test_1 = require("@playwright/test");
|
|
6
|
-
const node_path_1 = require("node:path");
|
|
3
|
+
exports.SiteWideTestMatch = exports.MobileOnlyTestMatch = exports.DesktopOnlyTestMatch = exports.SharedTestMatch = exports.ProjectTypes = exports.MobileBreakpoints = exports.DesktopBreakpoints = void 0;
|
|
7
4
|
// Section 3 of the Landing Page Test Plan — breakpoints / viewports.
|
|
8
|
-
exports.
|
|
9
|
-
MobileS: { name: "Mobile S", width: 320, height: 568 },
|
|
10
|
-
MobileM: { name: "Mobile M", width: 390, height: 844 },
|
|
11
|
-
Tablet: { name: "Tablet", width: 768, height: 1024 },
|
|
5
|
+
exports.DesktopBreakpoints = {
|
|
12
6
|
Laptop: { name: "Laptop", width: 1280, height: 800 },
|
|
13
7
|
Desktop: { name: "Desktop", width: 1440, height: 900 },
|
|
14
8
|
Wide: { name: "Wide", width: 1920, height: 1080 },
|
|
15
9
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
exports.MobileBreakpoints = {
|
|
11
|
+
MobileS: { name: "Mobile S", width: 320, height: 568 },
|
|
12
|
+
MobileM: { name: "Mobile M", width: 390, height: 844 },
|
|
13
|
+
Tablet: { name: "Tablet", width: 768, height: 1024 },
|
|
14
|
+
};
|
|
15
|
+
exports.ProjectTypes = {
|
|
22
16
|
Mobile: "mobile",
|
|
23
17
|
Desktop: "desktop",
|
|
18
|
+
Shared: "shared",
|
|
24
19
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
testMatch: [
|
|
30
|
-
SharedTestMatch,
|
|
31
|
-
configuration.projectType === ProjectTypes.Desktop ? DesktopOnlyTestMatch : MobileOnlyTestMatch,
|
|
32
|
-
],
|
|
33
|
-
use: {
|
|
34
|
-
...test_1.devices[device],
|
|
35
|
-
viewport: { width: configuration.breakpoint.width, height: configuration.breakpoint.height },
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
20
|
+
exports.SharedTestMatch = "shared/**/*.spec.js";
|
|
21
|
+
exports.DesktopOnlyTestMatch = "desktop-only/**/*.spec.js";
|
|
22
|
+
exports.MobileOnlyTestMatch = "mobile-only/**/*.spec.js";
|
|
23
|
+
exports.SiteWideTestMatch = "site-wide/**/*.spec.js";
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Interaction kind for filling a form field. */
|
|
3
|
+
export declare const FieldInteractions: {
|
|
4
|
+
/** A plain input */
|
|
5
|
+
readonly Fill: "fill";
|
|
6
|
+
/** A popup-based date/time picker widget */
|
|
7
|
+
readonly Datetime: "datetime";
|
|
8
|
+
};
|
|
9
|
+
export type FieldInteraction = (typeof FieldInteractions)[keyof typeof FieldInteractions];
|
|
10
|
+
/**
|
|
11
|
+
* One field within one form.
|
|
12
|
+
* A field only needs the properties relevant to what should be tested on it .
|
|
13
|
+
* (e.g. a field with no `invalidValue` is simply skipped by the invalid-input check for that form).
|
|
14
|
+
*/
|
|
15
|
+
declare const FormFieldSchema: z.ZodObject<{
|
|
16
|
+
testID: z.ZodString;
|
|
17
|
+
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
|
+
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
19
|
+
/** A plain input */
|
|
20
|
+
readonly Fill: "fill";
|
|
21
|
+
/** A popup-based date/time picker widget */
|
|
22
|
+
readonly Datetime: "datetime";
|
|
23
|
+
}>>>;
|
|
24
|
+
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
|
+
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
26
|
+
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
|
+
}, z.core.$strip>;
|
|
28
|
+
export type ManufacTestFormField = z.infer<typeof FormFieldSchema>;
|
|
29
|
+
/**
|
|
30
|
+
* Represents One form to be tested
|
|
31
|
+
*/
|
|
32
|
+
declare const FormSchema: z.ZodObject<{
|
|
33
|
+
testID: z.ZodString;
|
|
34
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
35
|
+
testID: z.ZodString;
|
|
36
|
+
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
+
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
38
|
+
/** A plain input */
|
|
39
|
+
readonly Fill: "fill";
|
|
40
|
+
/** A popup-based date/time picker widget */
|
|
41
|
+
readonly Datetime: "datetime";
|
|
42
|
+
}>>>;
|
|
43
|
+
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
45
|
+
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
|
+
}, z.core.$strip>>;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
export type ManufacTestForm = z.infer<typeof FormSchema>;
|
|
49
|
+
/**
|
|
50
|
+
* Defines selectors for commonly tested components.
|
|
51
|
+
*/
|
|
52
|
+
declare const ManufacTestSelectorsSchema: z.ZodObject<{
|
|
53
|
+
hamburgerTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
54
|
+
ctaTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
55
|
+
cookieAcceptTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
56
|
+
homeLinkTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
carouselSlideTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
carouselDotTestID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export type ManufacTestSelectors = z.infer<typeof ManufacTestSelectorsSchema>;
|
|
2
61
|
export declare const ManufacTestConfigSchema: z.ZodObject<{
|
|
3
62
|
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
|
+
forms: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
72
|
+
testID: z.ZodString;
|
|
73
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
74
|
+
testID: z.ZodString;
|
|
75
|
+
validValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
76
|
+
interaction: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
77
|
+
/** A plain input */
|
|
78
|
+
readonly Fill: "fill";
|
|
79
|
+
/** A popup-based date/time picker widget */
|
|
80
|
+
readonly Datetime: "datetime";
|
|
81
|
+
}>>>;
|
|
82
|
+
invalidValue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
83
|
+
expectedAutocomplete: z.ZodOptional<z.ZodNullable<z.ZodCustom<import("react").HTMLInputAutoCompleteAttribute | undefined, import("react").HTMLInputAutoCompleteAttribute | undefined>>>;
|
|
84
|
+
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
|
+
}, z.core.$strip>>;
|
|
86
|
+
}, z.core.$strip>>>>;
|
|
4
87
|
}, z.core.$strip>;
|
|
5
88
|
export type ManufacTestConfig = z.infer<typeof ManufacTestConfigSchema>;
|
|
89
|
+
export {};
|
package/dist/schema/index.js
CHANGED
|
@@ -1,7 +1,68 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ManufacTestConfigSchema = void 0;
|
|
3
|
+
exports.ManufacTestConfigSchema = exports.FieldInteractions = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
+
/** Interaction kind for filling a form field. */
|
|
6
|
+
exports.FieldInteractions = {
|
|
7
|
+
/** A plain input */
|
|
8
|
+
Fill: "fill",
|
|
9
|
+
/** A popup-based date/time picker widget */
|
|
10
|
+
Datetime: "datetime",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* One field within one form.
|
|
14
|
+
* A field only needs the properties relevant to what should be tested on it .
|
|
15
|
+
* (e.g. a field with no `invalidValue` is simply skipped by the invalid-input check for that form).
|
|
16
|
+
*/
|
|
17
|
+
const FormFieldSchema = zod_1.z.object({
|
|
18
|
+
/** data-testid of the input element itself. */
|
|
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. */
|
|
21
|
+
validValue: zod_1.z.string().nullish(),
|
|
22
|
+
/** How to interact with (or fill) this input */
|
|
23
|
+
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
|
+
invalidValue: zod_1.z.string().nullish(),
|
|
29
|
+
// `autoComplete` and `inputMode` values are not validated by schema. Only type inference is added.
|
|
30
|
+
/** Expected `autocomplete` attribute value, checked by the autofill spec. Omit to skip that check. */
|
|
31
|
+
expectedAutocomplete: zod_1.z.custom().nullish(),
|
|
32
|
+
/** Expected `inputmode` attribute value, checked by the soft-keyboard spec. Omit to skip that check. */
|
|
33
|
+
expectedInputMode: zod_1.z.custom().nullish(),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Represents One form to be tested
|
|
37
|
+
*/
|
|
38
|
+
const FormSchema = zod_1.z.object({
|
|
39
|
+
/** data-testid of the `<form>` element (or its closest wrapping container). */
|
|
40
|
+
testID: zod_1.z.string().min(1),
|
|
41
|
+
/** Fields belonging to this form. */
|
|
42
|
+
fields: zod_1.z.array(FormFieldSchema).min(1),
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Defines selectors for commonly tested components.
|
|
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
|
+
});
|
|
5
61
|
exports.ManufacTestConfigSchema = zod_1.z.object({
|
|
62
|
+
/** Pages this suite should run against. */
|
|
6
63
|
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
|
+
forms: zod_1.z.array(FormSchema).nullish(),
|
|
7
68
|
});
|
|
@@ -16,10 +16,10 @@ fixtures_1.test.describe("Forms - Input Edge Cases", () => {
|
|
|
16
16
|
await nameField.first().fill(longName);
|
|
17
17
|
const isOverflowingBeforeSubmit = await (0, utils_1.hasHorizontalScrollbar)(page);
|
|
18
18
|
(0, test_1.expect)(isOverflowingBeforeSubmit, "Page should not have overflowing/broken layout after a very long name is entered").toBe(false);
|
|
19
|
-
const submitButton = page.
|
|
19
|
+
const submitButton = page.locator('button[type="submit"], input[type="submit"]').first();
|
|
20
20
|
await (0, test_1.expect)(submitButton, "Submit button should remain visible with a long name entered").toBeVisible();
|
|
21
21
|
await submitButton.click();
|
|
22
|
-
const successState = page.getByText(/thank you|success|confirmation/i);
|
|
22
|
+
const successState = page.getByText(/thank you|success|confirmation/i).first();
|
|
23
23
|
await (0, test_1.expect)(successState, "Form should submit successfully with a very long name entered").toBeVisible();
|
|
24
24
|
const isOverflowingAfterSubmit = await (0, utils_1.hasHorizontalScrollbar)(page);
|
|
25
25
|
(0, test_1.expect)(isOverflowingAfterSubmit, "Confirmation should not have overflowing/broken layout after a very long name was submitted").toBe(false);
|
|
@@ -41,6 +41,10 @@ fixtures_1.test.describe("Forms - Input Edge Cases", () => {
|
|
|
41
41
|
});
|
|
42
42
|
(0, fixtures_1.test)("FM-08 Email with leading/trailing spaces is trimmed and submits successfully", async ({ page, targetPage }) => {
|
|
43
43
|
await page.goto(targetPage);
|
|
44
|
+
const form = page.locator("form");
|
|
45
|
+
if ((await form.count()) === 0) {
|
|
46
|
+
fixtures_1.test.skip(true, "No form present on this page");
|
|
47
|
+
}
|
|
44
48
|
await (0, utils_1.fillFormFields)(page);
|
|
45
49
|
const emailField = page.getByLabel(/email/i);
|
|
46
50
|
await emailField.fill(utils_1.ValidUserInfo.PaddedTestEmail);
|
|
@@ -49,8 +53,8 @@ fixtures_1.test.describe("Forms - Input Edge Cases", () => {
|
|
|
49
53
|
// server-side at submission time (also valid per the plan's wording) wouldn't show it here.
|
|
50
54
|
const trimmedEmail = await emailField.inputValue();
|
|
51
55
|
(0, test_1.expect)(trimmedEmail, `Leading/trailing spaces around the email should be trimmed; field still shows "${trimmedEmail}"`).toBe(utils_1.ValidUserInfo.PaddedTestEmail.trim());
|
|
52
|
-
await page.
|
|
53
|
-
const successState = page.getByText(/thank you|success|confirmation/i);
|
|
56
|
+
await page.locator('button[type="submit"], input[type="submit"]').first().click();
|
|
57
|
+
const successState = page.getByText(/thank you|success|confirmation/i).first();
|
|
54
58
|
await (0, test_1.expect)(successState, "Form should submit successfully after trimming leading/trailing spaces around the email").toBeVisible();
|
|
55
59
|
});
|
|
56
60
|
});
|
|
@@ -21,9 +21,7 @@ fixtures_1.test.describe("Forms - Invalid Input", () => {
|
|
|
21
21
|
// Fill invalid value in the field under test
|
|
22
22
|
await fieldLocator.first().fill(invalidCase.invalidValue);
|
|
23
23
|
// Attempt submit
|
|
24
|
-
const submitButton = form.
|
|
25
|
-
name: /(schedule discovery call|submit|send|book|request|get in touch)/i,
|
|
26
|
-
});
|
|
24
|
+
const submitButton = form.locator('button[type="submit"], input[type="submit"]');
|
|
27
25
|
await submitButton.click();
|
|
28
26
|
// Assert rejection - confirmation should NOT appear
|
|
29
27
|
const confirmation = page.getByText(/thank you|success|confirmation/i);
|
|
@@ -18,12 +18,19 @@ fixtures_1.test.describe("Forms", () => {
|
|
|
18
18
|
if (!isRequired && !isEmailField) {
|
|
19
19
|
fixtures_1.test.skip(true, `${targetField.label.source} field is not marked required on this page`);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Fill every OTHER required field validly and deliberately leave the target field
|
|
22
|
+
// untouched, rather than filling it and clearing it afterward — some picker widgets
|
|
23
|
+
// (e.g. a date/time field backed by a <button>, not a native input) can't be cleared
|
|
24
|
+
// with .clear() at all, so never filling them in the first place avoids that entirely.
|
|
25
|
+
await (0, utils_1.fillOtherRequiredFieldsValidly)(page, targetField.label);
|
|
23
26
|
const submitButtons = page.locator('button[type="submit"], input[type="submit"]');
|
|
24
27
|
await submitButtons.first().click();
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
// The error may render a moment after the click resolves (e.g. a brief re-render before
|
|
29
|
+
// the field's aria-describedby/error text attaches), so poll rather than check once.
|
|
30
|
+
await (0, test_1.expect)(async () => {
|
|
31
|
+
const isErrorPresent = await (0, utils_1.hasVisibleErrorNearField)(fieldLocator.first());
|
|
32
|
+
(0, test_1.expect)(isErrorPresent, `No visible inline error next to the empty ${targetField.label.source} field after submit`).toBe(true);
|
|
33
|
+
}).toPass({ timeout: 5000 });
|
|
27
34
|
const genericError = page.getByText(/form error/i);
|
|
28
35
|
const genericErrorCount = await genericError.count();
|
|
29
36
|
(0, test_1.expect)(genericErrorCount, `Error message for ${targetField.label.source} should be specific (e.g. "Email is required"), not a generic "form error"`).toBe(0);
|
|
@@ -6,9 +6,16 @@ const test_1 = require("@playwright/test");
|
|
|
6
6
|
fixtures_1.test.describe("Forms - Thank You Confirm", () => {
|
|
7
7
|
(0, fixtures_1.test)("FM-07 Confirmation state matches spec and does not resubmit on Back/Forward", async ({ page, targetPage }) => {
|
|
8
8
|
await page.goto(targetPage);
|
|
9
|
+
const form = page.locator("form");
|
|
10
|
+
if ((await form.count()) === 0) {
|
|
11
|
+
fixtures_1.test.skip(true, "No form present on this page");
|
|
12
|
+
}
|
|
9
13
|
await (0, utils_1.fillFormFields)(page);
|
|
10
|
-
await page.
|
|
11
|
-
|
|
14
|
+
await page.locator('button[type="submit"], input[type="submit"]').first().click();
|
|
15
|
+
// A toast/notification commonly has both a title and a description that separately match
|
|
16
|
+
// this pattern (e.g. "Success" + "...submitted successfully") — .first() avoids a strict-mode
|
|
17
|
+
// violation when that happens, rather than assuming there's ever only one matching element.
|
|
18
|
+
const confirmation = page.getByText(/thank you|success|confirmation/i).first();
|
|
12
19
|
await (0, test_1.expect)(confirmation, "Confirmation message/page did not appear or did not match the design spec").toBeVisible({ timeout: 10_000 });
|
|
13
20
|
let hasResubmitDialog = false;
|
|
14
21
|
page.on("dialog", (dialog) => {
|
|
@@ -17,8 +24,11 @@ fixtures_1.test.describe("Forms - Thank You Confirm", () => {
|
|
|
17
24
|
});
|
|
18
25
|
await page.goBack();
|
|
19
26
|
(0, test_1.expect)(hasResubmitDialog, 'Browser Back should NOT show a "resubmit form?" dialog').toBe(false);
|
|
27
|
+
// Not re-checking that the confirmation is still visible here: a toast-style confirmation
|
|
28
|
+
// (as opposed to a persistent thank-you page) is expected to auto-dismiss after a few
|
|
29
|
+
// seconds, and the Back/Forward round trip can easily take longer than that. The actual
|
|
30
|
+
// FM-07 concern — no resubmission — is fully covered by the dialog check below.
|
|
20
31
|
await page.goForward();
|
|
21
|
-
await (0, test_1.expect)(confirmation, "Confirmation should still be shown after pressing Forward").toBeVisible();
|
|
22
32
|
(0, test_1.expect)(hasResubmitDialog, 'Browser Forward should NOT show a "resubmit form?" dialog').toBe(false);
|
|
23
33
|
});
|
|
24
34
|
});
|
|
@@ -2,152 +2,30 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const fixtures_1 = require("../../fixtures");
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
-
fixtures_1.test.describe("
|
|
6
|
-
(0, fixtures_1.test)("LP-01: Page
|
|
5
|
+
fixtures_1.test.describe("Page load and structure", () => {
|
|
6
|
+
(0, fixtures_1.test)("LP-01: Page loads without errors", async ({ page, consoleErrors, targetPage }) => {
|
|
7
7
|
let failedNetworkRequests = 0;
|
|
8
8
|
page.on("response", (response) => {
|
|
9
9
|
const status = response.status();
|
|
10
|
-
// Track any 4xx or 5xx responses (except for the 404 test itself, but this is LP-01)
|
|
11
10
|
if (status >= 400) {
|
|
12
11
|
failedNetworkRequests += 1;
|
|
13
12
|
}
|
|
14
13
|
});
|
|
15
14
|
const response = await page.goto(targetPage);
|
|
16
|
-
// Page returns 200
|
|
17
15
|
(0, test_1.expect)(response?.ok()).toBe(true);
|
|
18
|
-
// Renders main heading (H1)
|
|
19
|
-
const h1 = page.getByRole("heading", { level: 1 }).first();
|
|
20
|
-
await (0, test_1.expect)(h1).toBeVisible();
|
|
21
|
-
// No console errors on load
|
|
22
16
|
(0, test_1.expect)(consoleErrors).toHaveLength(0);
|
|
23
|
-
// No failed asset/network requests
|
|
24
17
|
(0, test_1.expect)(failedNetworkRequests, "Page should load without any failed network requests (404/500)").toBe(0);
|
|
25
18
|
});
|
|
26
|
-
(0, fixtures_1.test)("LP-02: Correct page metadata
|
|
19
|
+
(0, fixtures_1.test)("LP-02: Correct page metadata", async ({ page, targetPage }) => {
|
|
27
20
|
await page.goto(targetPage);
|
|
28
|
-
// Title should not be empty, a raw URL, or a placeholder
|
|
29
21
|
const title = await page.title();
|
|
30
22
|
const hasTitle = title.trim().length > 0;
|
|
31
23
|
(0, test_1.expect)(hasTitle, "Page title should not be empty").toBe(true);
|
|
32
24
|
(0, test_1.expect)(title).not.toMatch(/^http/i);
|
|
33
25
|
(0, test_1.expect)(title).not.toMatch(/untitled/i);
|
|
34
|
-
// Favicon should be present
|
|
35
26
|
const favicon = page.locator('link[rel="icon"], link[rel="shortcut icon"]');
|
|
36
27
|
await (0, test_1.expect)(favicon.first()).toBeAttached();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// We can only reliably test HTTPS redirection if we have an absolute URL pointing to a remote environment.
|
|
40
|
-
// If targetPage is a relative path (like '/'), we skip because local dev servers usually run on HTTP.
|
|
41
|
-
const isRelativePath = targetPage.startsWith("/");
|
|
42
|
-
const isLocalhost = targetPage.includes("localhost");
|
|
43
|
-
const isLocalIP = targetPage.includes("127.0.0.1");
|
|
44
|
-
const isLocalEnvironment = isRelativePath || isLocalhost || isLocalIP;
|
|
45
|
-
if (isLocalEnvironment) {
|
|
46
|
-
fixtures_1.test.skip(true, "HTTPS redirection is only testable on remote staging/production environments");
|
|
47
|
-
}
|
|
48
|
-
let httpURL = targetPage;
|
|
49
|
-
const isHTTPS = targetPage.startsWith("https://");
|
|
50
|
-
const isHTTP = targetPage.startsWith("http://");
|
|
51
|
-
if (isHTTPS) {
|
|
52
|
-
httpURL = targetPage.replace("https://", "http://");
|
|
53
|
-
}
|
|
54
|
-
else if (!isHTTP) {
|
|
55
|
-
// isHTTP is true when the targetPage already explicitly defines the HTTP protocol
|
|
56
|
-
httpURL = `http://${targetPage}`;
|
|
57
|
-
}
|
|
58
|
-
await page.goto(httpURL);
|
|
59
|
-
// Address bar shows https:// (auto-redirected)
|
|
60
|
-
(0, test_1.expect)(page.url()).toMatch(/^https:\/\//);
|
|
61
|
-
});
|
|
62
|
-
(0, fixtures_1.test)("LP-04: 404/error handling", async ({ page, targetPage }) => {
|
|
63
|
-
let notFoundURL = `${targetPage}/thispagedoesnotexist`;
|
|
64
|
-
const hasTrailingSlash = targetPage.endsWith("/");
|
|
65
|
-
if (hasTrailingSlash) {
|
|
66
|
-
notFoundURL = `${targetPage}thispagedoesnotexist`;
|
|
67
|
-
}
|
|
68
|
-
await page.goto(notFoundURL);
|
|
69
|
-
// Check that it's a branded/styled 404 page, NOT a plain white server error page or a stack trace
|
|
70
|
-
const content = await page.content();
|
|
71
|
-
(0, test_1.expect)(content).not.toMatch(/stack trace/i);
|
|
72
|
-
// Look for common 404 indicators
|
|
73
|
-
const errorHeading = page.getByRole("heading").filter({ hasText: /404|not found|does not exist|not available/i });
|
|
74
|
-
const errorText = page.locator("text=/404|not found|does not exist|not available/i");
|
|
75
|
-
const hasErrorHeading = (await errorHeading.count()) > 0;
|
|
76
|
-
const hasErrorText = (await errorText.count()) > 0;
|
|
77
|
-
const isBranded404 = hasErrorHeading || hasErrorText;
|
|
78
|
-
(0, test_1.expect)(isBranded404, "Page should display a branded 404 error message").toBe(true);
|
|
79
|
-
// 404 page should have a link back to the home page
|
|
80
|
-
const homeLink = page.getByRole("link", { name: /home|back|return/i }).or(page.locator('a[href="/"]'));
|
|
81
|
-
const homeLinkCount = await homeLink.count();
|
|
82
|
-
const hasHomeLink = homeLinkCount > 0;
|
|
83
|
-
(0, test_1.expect)(hasHomeLink, "404 page should have a link back to the home page").toBe(true);
|
|
84
|
-
});
|
|
85
|
-
(0, fixtures_1.test)("RS-01: No horizontal overflow (responsive layout stability)", async ({ page, targetPage }) => {
|
|
86
|
-
await page.goto(targetPage);
|
|
87
|
-
const hasHorizontalOverflow = await page.evaluate(() => {
|
|
88
|
-
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
89
|
-
});
|
|
90
|
-
(0, test_1.expect)(hasHorizontalOverflow).toBe(false);
|
|
91
|
-
});
|
|
92
|
-
(0, fixtures_1.test)("FM-01: Form happy path submits successfully", async ({ page, targetPage, consoleErrors }) => {
|
|
93
|
-
await page.goto(targetPage);
|
|
94
|
-
const forms = page.locator("form");
|
|
95
|
-
const totalFormCount = await forms.count();
|
|
96
|
-
if (totalFormCount === 0) {
|
|
97
|
-
// test.skip aborts execution internally, so no else block is needed
|
|
98
|
-
fixtures_1.test.skip(true, "No form found on the page");
|
|
99
|
-
}
|
|
100
|
-
const allForms = await forms.all();
|
|
101
|
-
await Promise.all(allForms.map(async (form) => {
|
|
102
|
-
// Attempt to fill email inputs
|
|
103
|
-
const emailInputs = await form.getByRole("textbox", { name: /email/i }).all();
|
|
104
|
-
await Promise.all(emailInputs.map(async (emailInput) => {
|
|
105
|
-
const email = `qa+${Date.now().toString()}@manufacanalytics.com`;
|
|
106
|
-
await emailInput.fill(email);
|
|
107
|
-
}));
|
|
108
|
-
// Fill required text inputs
|
|
109
|
-
const textInputs = await form
|
|
110
|
-
.getByRole("textbox")
|
|
111
|
-
.filter({ hasNot: form.getByRole("textbox", { name: /email/i }) })
|
|
112
|
-
.all();
|
|
113
|
-
await Promise.all(textInputs.map(async (textInput) => {
|
|
114
|
-
const isRequired = await textInput.evaluate((element) => {
|
|
115
|
-
let isElementRequired = false;
|
|
116
|
-
if (element instanceof HTMLInputElement) {
|
|
117
|
-
isElementRequired = element.required;
|
|
118
|
-
}
|
|
119
|
-
return isElementRequired;
|
|
120
|
-
});
|
|
121
|
-
if (isRequired) {
|
|
122
|
-
await textInput.fill("Test User");
|
|
123
|
-
}
|
|
124
|
-
}));
|
|
125
|
-
// Set up the network response listener before submitting
|
|
126
|
-
const responsePromise = page.waitForResponse((response) => {
|
|
127
|
-
const method = response.request().method();
|
|
128
|
-
return ["POST", "PUT", "PATCH"].includes(method) || response.request().isNavigationRequest();
|
|
129
|
-
});
|
|
130
|
-
// Submit form
|
|
131
|
-
const submitButton = form.getByRole("button", { name: /submit|send|join|subscribe|save|draft|sign/i });
|
|
132
|
-
const submitButtonCount = await submitButton.count();
|
|
133
|
-
// It is possibility that form might have multiple submit buttons Ref: bud time entry form has 2 submit buttons
|
|
134
|
-
if (submitButtonCount > 0) {
|
|
135
|
-
await submitButton.first().click();
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
await form.evaluate((formElement) => {
|
|
139
|
-
if (formElement instanceof HTMLFormElement) {
|
|
140
|
-
formElement.submit();
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
// Wait for the response and verify it was successful
|
|
145
|
-
const response = await responsePromise;
|
|
146
|
-
(0, test_1.expect)(response.ok()).toBe(true);
|
|
147
|
-
}));
|
|
148
|
-
// Brief settle time for post-submit JS (redirects, toasts, error handlers)
|
|
149
|
-
await page.waitForTimeout(1000);
|
|
150
|
-
// No console errors after any form submission
|
|
151
|
-
(0, test_1.expect)(consoleErrors).toHaveLength(0);
|
|
28
|
+
const href = await favicon.first().getAttribute("href");
|
|
29
|
+
(0, test_1.expect)(href, "Favicon href should not be empty").toBeTruthy();
|
|
152
30
|
});
|
|
153
31
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
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("Page load and structure - site-wide", () => {
|
|
6
|
+
(0, fixtures_1.test)("LP-03: HTTPS enforced", async ({ page, targetPage, baseURL }) => {
|
|
7
|
+
const isLocalhost = baseURL?.includes("localhost");
|
|
8
|
+
const isLocalIP = baseURL?.includes("127.0.0.1");
|
|
9
|
+
const isLocalEnvironment = isLocalhost === true || isLocalIP === true;
|
|
10
|
+
if (isLocalEnvironment) {
|
|
11
|
+
fixtures_1.test.skip(true, "HTTPS redirection is only testable on remote staging/production environments");
|
|
12
|
+
}
|
|
13
|
+
let httpBaseURL = baseURL;
|
|
14
|
+
const isHTTPS = baseURL?.startsWith("https://");
|
|
15
|
+
const isHTTP = baseURL?.startsWith("http://");
|
|
16
|
+
if (isHTTPS === true && baseURL !== undefined) {
|
|
17
|
+
httpBaseURL = baseURL.replace("https://", "http://");
|
|
18
|
+
}
|
|
19
|
+
else if (isHTTP === false && baseURL !== undefined) {
|
|
20
|
+
httpBaseURL = `http://${baseURL}`;
|
|
21
|
+
}
|
|
22
|
+
const httpURL = new URL(targetPage, httpBaseURL).toString();
|
|
23
|
+
await page.goto(httpURL);
|
|
24
|
+
(0, test_1.expect)(page.url()).toMatch(/^https:\/\//);
|
|
25
|
+
});
|
|
26
|
+
(0, fixtures_1.test)("LP-04: 404/error handling", async ({ page, targetPage }) => {
|
|
27
|
+
// Avoid a doubled leading slash (e.g. "/" + "/x" -> "//x"), which browsers resolve as
|
|
28
|
+
// protocol-relative and treat "x" as a hostname instead of a path on this site.
|
|
29
|
+
const normalizedTargetPage = targetPage.endsWith("/") ? targetPage.slice(0, -1) : targetPage;
|
|
30
|
+
const notFoundURL = `${normalizedTargetPage}/thispagedoesnotexist`;
|
|
31
|
+
await page.goto(notFoundURL);
|
|
32
|
+
const content = await page.content();
|
|
33
|
+
(0, test_1.expect)(content).not.toMatch(/stack trace/i);
|
|
34
|
+
await (0, test_1.expect)(page.getByText("Page not found"), "404 page should show a branded message").toBeVisible();
|
|
35
|
+
await (0, test_1.expect)(page.getByRole("img", { name: /manufac|logo/i }), "404 page should show the company logo").toBeVisible();
|
|
36
|
+
await (0, test_1.expect)(page.getByRole("link", { name: /home|back|return/i }), "404 page should have a link back to the home page").toBeVisible();
|
|
37
|
+
});
|
|
38
|
+
});
|
package/dist/specs/utils.js
CHANGED
|
@@ -462,12 +462,13 @@ exports.FieldValueMappings = [
|
|
|
462
462
|
{ label: /date/i, validValue: exports.ValidUserInfo.ValidTestPreferredDate, interaction: exports.FieldInteractions.Datetime },
|
|
463
463
|
{ label: /time/i, validValue: exports.ValidUserInfo.ValidTestPreferredTime, interaction: exports.FieldInteractions.Datetime },
|
|
464
464
|
];
|
|
465
|
-
// Some date/time fields are plain inputs
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
465
|
+
// Some date/time fields are plain inputs; others are picker widgets that open a popup and
|
|
466
|
+
// reject typed text. Tries .fill() first, falling back to ARIA roles (grid/gridcell, spinbutton)
|
|
467
|
+
// if the value didn't take, since apps vary in UI library. No-ops if nothing matches.
|
|
468
|
+
//
|
|
469
|
+
// Mantine's DateTimePicker doesn't use role=gridcell for its calendar days (just plain <button>
|
|
470
|
+
// elements labeled by day number), so the day selector also falls back to any enabled button
|
|
471
|
+
// with a 1-2 digit label.
|
|
471
472
|
async function fillDateTimePicker(locator, fallbackValue) {
|
|
472
473
|
await locator.fill(fallbackValue).catch(() => {
|
|
473
474
|
return undefined;
|
|
@@ -478,7 +479,19 @@ async function fillDateTimePicker(locator, fallbackValue) {
|
|
|
478
479
|
if (filledValue.length === 0) {
|
|
479
480
|
await locator.click();
|
|
480
481
|
const page = locator.page();
|
|
481
|
-
|
|
482
|
+
// Give the popup a moment to finish laying out before interacting — clicking immediately
|
|
483
|
+
// can hit a day cell while another control (e.g. an AM/PM toggle) is still animating into
|
|
484
|
+
// place on top of it, intercepting the click.
|
|
485
|
+
await page.waitForTimeout(300);
|
|
486
|
+
// Scope the numeric-button fallback to an open popup/dialog when one exists, rather than the
|
|
487
|
+
// whole page, so it can't accidentally match an unrelated numeric button elsewhere (e.g. a
|
|
488
|
+
// pagination control or step counter).
|
|
489
|
+
const popup = page.locator('[role="dialog"], [role="listbox"], [aria-modal="true"]').first();
|
|
490
|
+
const hasPopup = (await popup.count()) > 0;
|
|
491
|
+
const numericDayButtons = (hasPopup ? popup : page)
|
|
492
|
+
.locator("button:not([disabled]):not([aria-disabled='true'])")
|
|
493
|
+
.filter({ hasText: /^\d{1,2}$/ });
|
|
494
|
+
const dayCells = page.locator('[role="gridcell"]:not([aria-disabled="true"])').or(numericDayButtons);
|
|
482
495
|
const dayCellCount = await dayCells.count();
|
|
483
496
|
for (let index = 0; index < dayCellCount; index++) {
|
|
484
497
|
const cell = dayCells.nth(index);
|
|
@@ -519,7 +532,10 @@ async function fillDateTimePicker(locator, fallbackValue) {
|
|
|
519
532
|
}
|
|
520
533
|
}
|
|
521
534
|
async function fillFormFields(page) {
|
|
522
|
-
|
|
535
|
+
// Filled sequentially, not concurrently — see the same note in fillOtherRequiredFieldsValidly:
|
|
536
|
+
// a popup-based widget open in one field can lose focus and close mid-interaction if another
|
|
537
|
+
// field is filled at the same time.
|
|
538
|
+
for (const field of exports.FieldValueMappings) {
|
|
523
539
|
const locator = page.getByLabel(field.label);
|
|
524
540
|
if ((await locator.count()) > 0) {
|
|
525
541
|
if (field.interaction === exports.FieldInteractions.Datetime) {
|
|
@@ -529,7 +545,7 @@ async function fillFormFields(page) {
|
|
|
529
545
|
await locator.first().fill(field.validValue);
|
|
530
546
|
}
|
|
531
547
|
}
|
|
532
|
-
}
|
|
548
|
+
}
|
|
533
549
|
}
|
|
534
550
|
async function hasVisibleErrorNearField(fieldLocator) {
|
|
535
551
|
const fieldID = await fieldLocator.getAttribute("id");
|
|
@@ -560,16 +576,35 @@ exports.InvalidInputCases = [
|
|
|
560
576
|
{ fieldLabel: /phone/i, invalidValue: "12", expectedErrorText: /enter a valid phone number/i },
|
|
561
577
|
];
|
|
562
578
|
async function fillOtherRequiredFieldsValidly(page, skipLabel) {
|
|
563
|
-
|
|
579
|
+
const skipLocator = page.getByLabel(skipLabel);
|
|
580
|
+
const skipFieldCount = await skipLocator.count();
|
|
581
|
+
// Some combined widgets (e.g. a single "Preferred Time & Date" picker) match more than one
|
|
582
|
+
// FieldValueMappings label at once — comparing by element id, not just label text, catches
|
|
583
|
+
// that case so filling the "other" field doesn't silently fill the one under test too.
|
|
584
|
+
const skipFieldID = skipFieldCount > 0 ? await skipLocator.first().getAttribute("id") : null;
|
|
585
|
+
// Filled sequentially, not concurrently: a popup-based widget (like a date/time picker) opens
|
|
586
|
+
// on focus and closes on blur, so filling a different field at the same time can steal focus
|
|
587
|
+
// and close it mid-interaction, detaching whatever this was about to click inside it.
|
|
588
|
+
for (const field of exports.FieldValueMappings) {
|
|
564
589
|
const isSkippedField = field.label.source === skipLabel.source;
|
|
565
590
|
const locator = page.getByLabel(field.label);
|
|
566
591
|
let isFieldPresent = false;
|
|
567
592
|
if (!isSkippedField) {
|
|
568
593
|
isFieldPresent = (await locator.count()) > 0;
|
|
569
594
|
}
|
|
595
|
+
let isSameElementAsSkipped = false;
|
|
596
|
+
if (isFieldPresent && skipFieldID !== null) {
|
|
597
|
+
const fieldID = await locator.first().getAttribute("id");
|
|
598
|
+
isSameElementAsSkipped = fieldID !== null && fieldID === skipFieldID;
|
|
599
|
+
}
|
|
600
|
+
// Email is always treated as required (see the FieldValueMappings doc comment above),
|
|
601
|
+
// regardless of whether it carries a native `required`/`aria-required` attribute — the
|
|
602
|
+
// same rule the isEmailField check in required-field.spec.ts applies for its own skip
|
|
603
|
+
// decision, so filling here needs to agree with it or both can end up empty at once.
|
|
604
|
+
const isEmailField = field.label.source === "email";
|
|
570
605
|
let isRequired = false;
|
|
571
|
-
if (isFieldPresent) {
|
|
572
|
-
isRequired = await isFieldRequired(locator.first());
|
|
606
|
+
if (isFieldPresent && !isSameElementAsSkipped) {
|
|
607
|
+
isRequired = isEmailField || (await isFieldRequired(locator.first()));
|
|
573
608
|
}
|
|
574
609
|
if (isRequired) {
|
|
575
610
|
if (field.interaction === exports.FieldInteractions.Datetime) {
|
|
@@ -579,7 +614,7 @@ async function fillOtherRequiredFieldsValidly(page, skipLabel) {
|
|
|
579
614
|
await locator.first().fill(field.validValue);
|
|
580
615
|
}
|
|
581
616
|
}
|
|
582
|
-
}
|
|
617
|
+
}
|
|
583
618
|
}
|
|
584
619
|
exports.AutofillFields = [
|
|
585
620
|
{ label: /name/i, expectedAutocomplete: /name/i },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manufac/general-tests",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A library for general tests for landing pages",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@manufac/eslint-config": "*",
|
|
28
28
|
"@manufac/prettier-config": "*",
|
|
29
|
-
"@playwright/test": "^1.62.
|
|
29
|
+
"@playwright/test": "^1.62.1",
|
|
30
|
+
"@types/react": "^19.2.18",
|
|
30
31
|
"typescript": "^7.0.2"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|