@manufac/general-tests 0.1.1 → 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.
@@ -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 Breakpoints: {
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 ProjectTypes: {
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 type ProjectType = (typeof ProjectTypes)[keyof typeof ProjectTypes];
45
- export interface MobileConfig {
46
- projectType: typeof ProjectTypes.Mobile;
47
- breakpoint: typeof Breakpoints.MobileM;
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";
@@ -1,38 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Breakpoints = void 0;
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.Breakpoints = {
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
- // Resolves to dist/specs at runtime so projects work regardless of the consuming app's own testDir.
17
- const SpecsDirectory = (0, node_path_1.join)(__dirname, "..", "specs");
18
- const SharedTestMatch = "shared/**/*.spec.js";
19
- const DesktopOnlyTestMatch = "desktop-only/**/*.spec.js";
20
- const MobileOnlyTestMatch = "mobile-only/**/*.spec.js";
21
- const ProjectTypes = {
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
- function createProject({ configuration, device, name }) {
26
- return {
27
- name,
28
- testDir: SpecsDirectory,
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";
@@ -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 {};
@@ -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
  });
@@ -2,168 +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("Landing Page Automated Smoke Suite", () => {
6
- (0, fixtures_1.test)("LP-01: Page returns 200, renders H1, and has no console errors", async ({ page, consoleErrors, targetPage, }, testInfo) => {
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
10
  if (status >= 400) {
11
- // A service worker registered in production but not built/served by a dev server is a
12
- // common, expected gap across apps using this package — surface it as a warning rather
13
- // than failing the test, since it doesn't indicate a real broken page.
14
- //
15
- // Observed directly while diagnosing this check: headless
16
- // WebKit actually issues the navigator.serviceWorker.register() fetch and gets a real
17
- // 404 for a missing file, while headless Chromium/Firefox never issue that fetch at
18
- // all — so the same missing file only ever surfaces as a failed request under WebKit.
19
- const isServiceWorkerRequest = response.url().includes("service-worker.js");
20
- if (isServiceWorkerRequest) {
21
- testInfo.annotations.push({
22
- type: "warning",
23
- description: `Service worker registration failed (${String(status)}) — likely not built by the dev server: ${response.url()}`,
24
- });
25
- }
26
- else {
27
- failedNetworkRequests += 1;
28
- }
11
+ failedNetworkRequests += 1;
29
12
  }
30
13
  });
31
14
  const response = await page.goto(targetPage);
32
- // Page returns 200
33
15
  (0, test_1.expect)(response?.ok()).toBe(true);
34
- // Renders main heading (H1)
35
- const h1 = page.getByRole("heading", { level: 1 }).first();
36
- await (0, test_1.expect)(h1).toBeVisible();
37
- // No console errors on load
38
16
  (0, test_1.expect)(consoleErrors).toHaveLength(0);
39
- // No failed asset/network requests
40
17
  (0, test_1.expect)(failedNetworkRequests, "Page should load without any failed network requests (404/500)").toBe(0);
41
18
  });
42
- (0, fixtures_1.test)("LP-02: Correct page metadata (title and favicon)", async ({ page, targetPage }) => {
19
+ (0, fixtures_1.test)("LP-02: Correct page metadata", async ({ page, targetPage }) => {
43
20
  await page.goto(targetPage);
44
- // Title should not be empty, a raw URL, or a placeholder
45
21
  const title = await page.title();
46
22
  const hasTitle = title.trim().length > 0;
47
23
  (0, test_1.expect)(hasTitle, "Page title should not be empty").toBe(true);
48
24
  (0, test_1.expect)(title).not.toMatch(/^http/i);
49
25
  (0, test_1.expect)(title).not.toMatch(/untitled/i);
50
- // Favicon should be present
51
26
  const favicon = page.locator('link[rel="icon"], link[rel="shortcut icon"]');
52
27
  await (0, test_1.expect)(favicon.first()).toBeAttached();
53
- });
54
- (0, fixtures_1.test)("LP-03: HTTPS enforced", async ({ page, targetPage }) => {
55
- // We can only reliably test HTTPS redirection if we have an absolute URL pointing to a remote environment.
56
- // If targetPage is a relative path (like '/'), we skip because local dev servers usually run on HTTP.
57
- const isRelativePath = targetPage.startsWith("/");
58
- const isLocalhost = targetPage.includes("localhost");
59
- const isLocalIP = targetPage.includes("127.0.0.1");
60
- const isLocalEnvironment = isRelativePath || isLocalhost || isLocalIP;
61
- if (isLocalEnvironment) {
62
- fixtures_1.test.skip(true, "HTTPS redirection is only testable on remote staging/production environments");
63
- }
64
- let httpURL = targetPage;
65
- const isHTTPS = targetPage.startsWith("https://");
66
- const isHTTP = targetPage.startsWith("http://");
67
- if (isHTTPS) {
68
- httpURL = targetPage.replace("https://", "http://");
69
- }
70
- else if (!isHTTP) {
71
- // isHTTP is true when the targetPage already explicitly defines the HTTP protocol
72
- httpURL = `http://${targetPage}`;
73
- }
74
- await page.goto(httpURL);
75
- // Address bar shows https:// (auto-redirected)
76
- (0, test_1.expect)(page.url()).toMatch(/^https:\/\//);
77
- });
78
- (0, fixtures_1.test)("LP-04: 404/error handling", async ({ page, targetPage }) => {
79
- let notFoundURL = `${targetPage}/thispagedoesnotexist`;
80
- const hasTrailingSlash = targetPage.endsWith("/");
81
- if (hasTrailingSlash) {
82
- notFoundURL = `${targetPage}thispagedoesnotexist`;
83
- }
84
- await page.goto(notFoundURL);
85
- // Check that it's a branded/styled 404 page, NOT a plain white server error page or a stack trace
86
- const content = await page.content();
87
- (0, test_1.expect)(content).not.toMatch(/stack trace/i);
88
- // Look for common 404 indicators
89
- const errorHeading = page.getByRole("heading").filter({ hasText: /404|not found|does not exist|not available/i });
90
- const errorText = page.locator("text=/404|not found|does not exist|not available/i");
91
- const hasErrorHeading = (await errorHeading.count()) > 0;
92
- const hasErrorText = (await errorText.count()) > 0;
93
- const isBranded404 = hasErrorHeading || hasErrorText;
94
- (0, test_1.expect)(isBranded404, "Page should display a branded 404 error message").toBe(true);
95
- // 404 page should have a link back to the home page
96
- const homeLink = page.getByRole("link", { name: /home|back|return/i }).or(page.locator('a[href="/"]'));
97
- const homeLinkCount = await homeLink.count();
98
- const hasHomeLink = homeLinkCount > 0;
99
- (0, test_1.expect)(hasHomeLink, "404 page should have a link back to the home page").toBe(true);
100
- });
101
- (0, fixtures_1.test)("RS-01: No horizontal overflow (responsive layout stability)", async ({ page, targetPage }) => {
102
- await page.goto(targetPage);
103
- const hasHorizontalOverflow = await page.evaluate(() => {
104
- return document.documentElement.scrollWidth > document.documentElement.clientWidth;
105
- });
106
- (0, test_1.expect)(hasHorizontalOverflow).toBe(false);
107
- });
108
- (0, fixtures_1.test)("FM-01: Form happy path submits successfully", async ({ page, targetPage, consoleErrors }) => {
109
- await page.goto(targetPage);
110
- const forms = page.locator("form");
111
- const totalFormCount = await forms.count();
112
- if (totalFormCount === 0) {
113
- // test.skip aborts execution internally, so no else block is needed
114
- fixtures_1.test.skip(true, "No form found on the page");
115
- }
116
- const allForms = await forms.all();
117
- await Promise.all(allForms.map(async (form) => {
118
- // Attempt to fill email inputs
119
- const emailInputs = await form.getByRole("textbox", { name: /email/i }).all();
120
- await Promise.all(emailInputs.map(async (emailInput) => {
121
- const email = `qa+${Date.now().toString()}@manufacanalytics.com`;
122
- await emailInput.fill(email);
123
- }));
124
- // Fill required text inputs
125
- const textInputs = await form
126
- .getByRole("textbox")
127
- .filter({ hasNot: form.getByRole("textbox", { name: /email/i }) })
128
- .all();
129
- await Promise.all(textInputs.map(async (textInput) => {
130
- const isRequired = await textInput.evaluate((element) => {
131
- let isElementRequired = false;
132
- if (element instanceof HTMLInputElement) {
133
- isElementRequired = element.required;
134
- }
135
- return isElementRequired;
136
- });
137
- if (isRequired) {
138
- await textInput.fill("Test User");
139
- }
140
- }));
141
- // Set up the network response listener before submitting
142
- const responsePromise = page.waitForResponse((response) => {
143
- const method = response.request().method();
144
- return ["POST", "PUT", "PATCH"].includes(method) || response.request().isNavigationRequest();
145
- });
146
- // Submit form
147
- const submitButton = form.getByRole("button", { name: /submit|send|join|subscribe|save|draft|sign/i });
148
- const submitButtonCount = await submitButton.count();
149
- // It is possibility that form might have multiple submit buttons Ref: bud time entry form has 2 submit buttons
150
- if (submitButtonCount > 0) {
151
- await submitButton.first().click();
152
- }
153
- else {
154
- await form.evaluate((formElement) => {
155
- if (formElement instanceof HTMLFormElement) {
156
- formElement.submit();
157
- }
158
- });
159
- }
160
- // Wait for the response and verify it was successful
161
- const response = await responsePromise;
162
- (0, test_1.expect)(response.ok()).toBe(true);
163
- }));
164
- // Brief settle time for post-submit JS (redirects, toasts, error handlers)
165
- await page.waitForTimeout(1000);
166
- // No console errors after any form submission
167
- (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();
168
30
  });
169
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manufac/general-tests",
3
- "version": "0.1.1",
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.0",
29
+ "@playwright/test": "^1.62.1",
30
+ "@types/react": "^19.2.18",
30
31
  "typescript": "^7.0.2"
31
32
  },
32
33
  "peerDependencies": {