@manufac/general-tests 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,74 +9,128 @@ 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@0.0.1-alpha
12
+ npm install @manufac/general-tests
13
13
  ```
14
14
 
15
- ```json
16
- {
17
- "dependencies": {
18
- "@manufac/general-tests": "0.0.1-alpha"
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 wire it into the app's `playwright.config.ts` using the project-builder helpers:
28
+ Then reference it from `playwright.config.ts`:
24
29
 
25
30
  ```ts
26
- import { defineConfig } from "@playwright/test";
27
- import { Breakpoints, createProject } from "@manufac/general-tests/config/projects.js";
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
- export default defineConfig({
30
- use: {
31
- baseURL: "http://localhost:8080",
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 },
32
52
  },
33
- projects: [
34
- createProject({
35
- name: "Desktop Chrome",
36
- device: "Desktop Chrome",
37
- configuration: { projectType: "desktop", breakpoint: Breakpoints.Desktop },
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,
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 },
64
+ },
65
+ {
66
+ name: "Responsive 375",
67
+ testDir: GeneralTestsSpecsDir,
68
+ testMatch: ResponsiveLayoutTestMatch,
69
+ use: { ...devices["Desktop Chrome"], viewport: ResponsiveLayoutBreakpoints.MobileL },
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,
64
84
  },
85
+ projects: Projects,
65
86
  });
66
87
  ```
67
88
 
68
- - `createProject({ name, device, configuration })` builds a Playwright project for the given device.
69
- - `configuration.projectType: "desktop"` runs the `shared` + `desktop-only` specs, and requires `configuration.breakpoint: Breakpoints.Desktop` (1440x900).
70
- - `configuration.projectType: "mobile"` runs the `shared` + `mobile-only` specs, and requires `configuration.breakpoint: Breakpoints.MobileM` (390x844).
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.
93
+
94
+ ## Storybook visual regression
95
+
96
+ `VisualRegressionTestMatch` runs `visual-regression.spec.ts` — a single generic test that walks every story in a running Storybook instance and compares it against a baseline screenshot with Playwright's `toHaveScreenshot()`. No per-component spec files are needed; new stories are picked up automatically.
71
97
 
72
- By default every spec runs against `/`. To point a project at a different page, override the `targetPage` fixture option:
98
+ It needs its own Playwright config, separate from the one above, since it runs against Storybook's dev server instead of the app itself:
73
99
 
74
100
  ```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" }
101
+ // playwright.storybook.config.ts
102
+ import { defineConfig, devices } from "@playwright/test";
103
+ import { dirname, join } from "node:path";
104
+ import { VisualRegressionTestMatch } from "@manufac/general-tests/config/projects.js";
105
+
106
+ const GeneralTestsSpecsDir = join(dirname(require.resolve("@manufac/general-tests/config/projects.js")), "..", "specs");
107
+
108
+ export default defineConfig({
109
+ testDir: GeneralTestsSpecsDir,
110
+ testMatch: VisualRegressionTestMatch,
111
+ // Relative, so Playwright resolves it against this config file's own directory (not
112
+ // testDir, which points into the installed package) — baselines stay in this repo.
113
+ snapshotPathTemplate: "e2e-visual/__screenshots__/{arg}{ext}",
114
+ reporter: [["html", { open: "never" }]],
115
+ expect: { toHaveScreenshot: { maxDiffPixelRatio: 0.001, animations: "disabled" } },
116
+ use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 }, baseURL: "http://localhost:6006" },
117
+ webServer: {
118
+ command: "npm run start:sb -- --ci --port 6006",
119
+ url: "http://localhost:6006",
120
+ reuseExistingServer: true,
121
+ },
122
+ });
82
123
  ```
124
+
125
+ Then add two scripts to the app's `package.json` (adjust `start:sb`/`build:sb` to however the app runs Storybook):
126
+
127
+ ```json
128
+ {
129
+ "scripts": {
130
+ "test:visual": "npm run build:sb && playwright test -c playwright.storybook.config.ts",
131
+ "update:visual": "npm run build:sb && playwright test -c playwright.storybook.config.ts --update-snapshots"
132
+ }
133
+ }
134
+ ```
135
+
136
+ Baselines land in `e2e-visual/__screenshots__/<story-id>.png`, next to `playwright.storybook.config.ts`.
@@ -32,5 +32,7 @@ export declare const ProjectTypes: {
32
32
  readonly Desktop: "desktop";
33
33
  readonly Shared: "shared";
34
34
  };
35
- export declare const LoadingPageTestMatch = "loading-page.spec.ts";
35
+ export declare const LoadingPageTestMatch = "loading-page.spec.js";
36
36
  export declare const ResponsiveLayoutTestMatch = "responsive-layout.spec.js";
37
+ export declare const FormsTestMatch = "forms.spec.js";
38
+ export declare const VisualRegressionTestMatch = "visual-regression.spec.js";
@@ -1,6 +1,6 @@
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.VisualRegressionTestMatch = 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 },
@@ -15,5 +15,7 @@ exports.ProjectTypes = {
15
15
  Desktop: "desktop",
16
16
  Shared: "shared",
17
17
  };
18
- exports.LoadingPageTestMatch = "loading-page.spec.ts";
18
+ exports.LoadingPageTestMatch = "loading-page.spec.js";
19
19
  exports.ResponsiveLayoutTestMatch = "responsive-layout.spec.js";
20
+ exports.FormsTestMatch = "forms.spec.js";
21
+ exports.VisualRegressionTestMatch = "visual-regression.spec.js";
@@ -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>;
@@ -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) => {
@@ -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
- testID: z.ZodString;
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
- testID: z.ZodString;
34
+ formID: z.ZodString;
35
+ page: z.ZodString;
34
36
  fields: z.ZodArray<z.ZodObject<{
35
- testID: z.ZodString;
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 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>;
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
- testID: z.ZodString;
76
+ formID: z.ZodString;
77
+ page: z.ZodString;
73
78
  fields: z.ZodArray<z.ZodObject<{
74
- testID: z.ZodString;
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,10 +85,18 @@ 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>;
96
+ export declare const StorybookIndexSchema: z.ZodObject<{
97
+ entries: z.ZodRecord<z.ZodString, z.ZodObject<{
98
+ type: z.ZodString;
99
+ id: z.ZodString;
100
+ }, z.core.$strip>>;
101
+ }, z.core.$strip>;
89
102
  export {};
@@ -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.StorybookIndexSchema = 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,32 @@ 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
- /** 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. */
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
- // `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. */
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
- /** 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. */
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
- * 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
- });
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
  });
41
+ exports.StorybookIndexSchema = zod_1.z.object({
42
+ entries: zod_1.z.record(zod_1.z.string(), zod_1.z.object({
43
+ type: zod_1.z.string(),
44
+ id: zod_1.z.string(),
45
+ })),
46
+ });
@@ -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
+ });
@@ -1,4 +1,5 @@
1
- import type { Locator, Page } from "@playwright/test";
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 {};
@@ -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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const schema_1 = require("../schema");
4
+ const test_1 = require("@playwright/test");
5
+ (0, test_1.test)("storybook stories match their baseline screenshots", async ({ page }) => {
6
+ // index.json is generated at runtime by the Storybook dev server (resolved against
7
+ // baseURL in the consuming app's playwright.storybook.config.ts), not a file in the repo.
8
+ const response = await page.request.get("index.json");
9
+ const result = schema_1.StorybookIndexSchema.safeParse(await response.json());
10
+ if (!result.success) {
11
+ throw new Error(`Failed to parse Storybook index: ${result.error.message}`);
12
+ }
13
+ const storyIDs = Object.values(result.data.entries)
14
+ .filter((entry) => {
15
+ return entry.type === "story";
16
+ })
17
+ .map((entry) => {
18
+ return entry.id;
19
+ });
20
+ const errors = [];
21
+ for (const storyID of storyIDs) {
22
+ await test_1.test.step(storyID, async () => {
23
+ try {
24
+ await page.goto(`iframe.html?id=${storyID}&viewMode=story`);
25
+ await (0, test_1.expect)(page.locator("#storybook-root")).toBeVisible();
26
+ await (0, test_1.expect)(page.locator(".sb-preparing-story .sb-loader")).toBeHidden(); // Avoid screenshotting a half-rendered story
27
+ await (0, test_1.expect)(page).toHaveScreenshot(`${storyID}.png`, { fullPage: true, timeout: 10_000 });
28
+ }
29
+ catch (error) {
30
+ // Collect failures instead of stopping at the first broken story.
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ errors.push(`Story ${storyID} failed: ${message}`);
33
+ }
34
+ });
35
+ }
36
+ (0, test_1.expect)(errors, `Visual regression failures:\n${errors.join("\n")}`).toHaveLength(0);
37
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manufac/general-tests",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "A library for general tests for landing pages",
5
5
  "repository": {
6
6
  "type": "git",