@manufac/general-tests 0.0.1-alpha

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.
Files changed (54) hide show
  1. package/README.md +58 -0
  2. package/dist/config/projects.d.ts +41 -0
  3. package/dist/config/projects.js +37 -0
  4. package/dist/fixtures/index.d.ts +5 -0
  5. package/dist/fixtures/index.js +27 -0
  6. package/dist/schema/index.d.ts +5 -0
  7. package/dist/schema/index.js +7 -0
  8. package/dist/specs/desktop-only/browser-zoom.spec.d.ts +1 -0
  9. package/dist/specs/desktop-only/browser-zoom.spec.js +18 -0
  10. package/dist/specs/mobile-only/navigation.spec.d.ts +1 -0
  11. package/dist/specs/mobile-only/navigation.spec.js +30 -0
  12. package/dist/specs/mobile-only/orientation.spec.d.ts +1 -0
  13. package/dist/specs/mobile-only/orientation.spec.js +43 -0
  14. package/dist/specs/mobile-only/readability.spec.d.ts +1 -0
  15. package/dist/specs/mobile-only/readability.spec.js +14 -0
  16. package/dist/specs/mobile-only/soft-keyboard.spec.d.ts +1 -0
  17. package/dist/specs/mobile-only/soft-keyboard.spec.js +34 -0
  18. package/dist/specs/mobile-only/touch-targets.spec.d.ts +1 -0
  19. package/dist/specs/mobile-only/touch-targets.spec.js +12 -0
  20. package/dist/specs/shared/cta.spec.d.ts +1 -0
  21. package/dist/specs/shared/cta.spec.js +199 -0
  22. package/dist/specs/shared/forms/autofill.spec.d.ts +1 -0
  23. package/dist/specs/shared/forms/autofill.spec.js +16 -0
  24. package/dist/specs/shared/forms/double-submit.spec.d.ts +1 -0
  25. package/dist/specs/shared/forms/double-submit.spec.js +39 -0
  26. package/dist/specs/shared/forms/input-edge.spec.d.ts +1 -0
  27. package/dist/specs/shared/forms/input-edge.spec.js +46 -0
  28. package/dist/specs/shared/forms/invalid-input.spec.d.ts +1 -0
  29. package/dist/specs/shared/forms/invalid-input.spec.js +24 -0
  30. package/dist/specs/shared/forms/required-field.spec.d.ts +1 -0
  31. package/dist/specs/shared/forms/required-field.spec.js +27 -0
  32. package/dist/specs/shared/forms/thankyou-confirm.spec.d.ts +1 -0
  33. package/dist/specs/shared/forms/thankyou-confirm.spec.js +25 -0
  34. package/dist/specs/shared/hero-fold.spec.d.ts +1 -0
  35. package/dist/specs/shared/hero-fold.spec.js +21 -0
  36. package/dist/specs/shared/image-quality.spec.d.ts +1 -0
  37. package/dist/specs/shared/image-quality.spec.js +13 -0
  38. package/dist/specs/shared/landing-page.spec.d.ts +1 -0
  39. package/dist/specs/shared/landing-page.spec.js +84 -0
  40. package/dist/specs/shared/media-embeds/carousel-slider.spec.d.ts +1 -0
  41. package/dist/specs/shared/media-embeds/carousel-slider.spec.js +31 -0
  42. package/dist/specs/shared/media-embeds/cookie-consent.spec.d.ts +1 -0
  43. package/dist/specs/shared/media-embeds/cookie-consent.spec.js +27 -0
  44. package/dist/specs/shared/media-embeds/third-party-embeds.spec.d.ts +1 -0
  45. package/dist/specs/shared/media-embeds/third-party-embeds.spec.js +24 -0
  46. package/dist/specs/shared/media-embeds/video-playback.spec.d.ts +1 -0
  47. package/dist/specs/shared/media-embeds/video-playback.spec.js +54 -0
  48. package/dist/specs/shared/sticky-elements.spec.d.ts +1 -0
  49. package/dist/specs/shared/sticky-elements.spec.js +13 -0
  50. package/dist/specs/sticky-utils.d.ts +13 -0
  51. package/dist/specs/sticky-utils.js +158 -0
  52. package/dist/specs/utils.d.ts +119 -0
  53. package/dist/specs/utils.js +529 -0
  54. package/package.json +30 -0
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ fixtures_1.test.describe("Forms - Input Edge Cases", () => {
7
+ (0, fixtures_1.test)("FM-08 Name field accepts a 200+ character string without breaking layout", async ({ page, targetPage }) => {
8
+ await page.goto(targetPage);
9
+ await (0, utils_1.fillAllFieldsValidly)(page);
10
+ const longName = "A".repeat(utils_1.ValidUserInfo.LongNameCharacterCount);
11
+ await page.getByLabel(/name/i).fill(longName);
12
+ const isOverflowingBeforeSubmit = await (0, utils_1.hasHorizontalScrollbar)(page);
13
+ (0, test_1.expect)(isOverflowingBeforeSubmit, "Page should not have overflowing/broken layout after a very long name is entered").toBe(false);
14
+ const submitButton = page.getByRole("button", { name: /submit/i });
15
+ await (0, test_1.expect)(submitButton, "Submit button should remain visible with a long name entered").toBeVisible();
16
+ await submitButton.click();
17
+ const successState = page.getByText(/thank you|success|confirmation/i);
18
+ await (0, test_1.expect)(successState, "Form should submit successfully with a very long name entered").toBeVisible();
19
+ const isOverflowingAfterSubmit = await (0, utils_1.hasHorizontalScrollbar)(page);
20
+ (0, test_1.expect)(isOverflowingAfterSubmit, "Confirmation should not have overflowing/broken layout after a very long name was submitted").toBe(false);
21
+ });
22
+ (0, fixtures_1.test)("FM-08 Name field with emoji is accepted or cleanly rejected", async ({ page, targetPage, consoleErrors }) => {
23
+ await page.goto(targetPage);
24
+ await page.getByLabel(/name/i).fill(utils_1.ValidUserInfo.EmojiTestName);
25
+ const nameValue = await page.getByLabel(/name/i).inputValue();
26
+ const isEmojiAccepted = nameValue === utils_1.ValidUserInfo.EmojiTestName;
27
+ // Indicates the emoji character was not preserved in the field value (stripped or cleared by the input).
28
+ const isEmojiRejected = nameValue === "" || !nameValue.includes("😀");
29
+ (0, test_1.expect)(isEmojiAccepted || isEmojiRejected, "Emoji in the name field should be accepted as typed or cleanly rejected (no garbled/crashed display)").toBe(true);
30
+ (0, test_1.expect)(consoleErrors, `Entering emoji in the name field should not cause a console error:\n${consoleErrors.join("\n")}`).toHaveLength(0);
31
+ });
32
+ (0, fixtures_1.test)("FM-08 Email with leading/trailing spaces is trimmed and submits successfully", async ({ page, targetPage }) => {
33
+ await page.goto(targetPage);
34
+ await (0, utils_1.fillAllFieldsValidly)(page);
35
+ const emailField = page.getByLabel(/email/i);
36
+ await emailField.fill(utils_1.ValidUserInfo.PaddedTestEmail);
37
+ await emailField.blur();
38
+ // Checked on blur since that's when most forms trim client-side; a form that only trims
39
+ // server-side at submission time (also valid per the plan's wording) wouldn't show it here.
40
+ const trimmedEmail = await emailField.inputValue();
41
+ (0, test_1.expect)(trimmedEmail, `Leading/trailing spaces around the email should be trimmed; field still shows "${trimmedEmail}"`).toBe(utils_1.ValidUserInfo.PaddedTestEmail.trim());
42
+ await page.getByRole("button", { name: /submit/i }).click();
43
+ const successState = page.getByText(/thank you|success|confirmation/i);
44
+ await (0, test_1.expect)(successState, "Form should submit successfully after trimming leading/trailing spaces around the email").toBeVisible();
45
+ });
46
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ // Ref: https://playwright.dev/docs/test-parameterize#parameterized-tests
7
+ fixtures_1.test.describe("Forms - Invalid Input", () => {
8
+ utils_1.InvalidInputCases.forEach((invalidCase) => {
9
+ (0, fixtures_1.test)(`FM-03 Invalid input "${invalidCase.invalidValue}" in ${invalidCase.fieldLabel.source} is rejected`, async ({ page, targetPage, }) => {
10
+ await page.goto(targetPage);
11
+ const targetField = page.getByLabel(invalidCase.fieldLabel);
12
+ const fieldCount = await targetField.count();
13
+ if (fieldCount === 0) {
14
+ fixtures_1.test.skip(true, `No ${invalidCase.fieldLabel.source} field present on this page`);
15
+ }
16
+ await (0, utils_1.fillOtherRequiredFieldsValidly)(page, invalidCase.fieldLabel);
17
+ await targetField.first().fill(invalidCase.invalidValue);
18
+ const submitButtons = page.locator('button[type="submit"], input[type="submit"]');
19
+ await submitButtons.first().click();
20
+ await (0, test_1.expect)(page.getByText(invalidCase.expectedErrorText), `Expected a clear inline error for "${invalidCase.invalidValue}" in ${invalidCase.fieldLabel.source}`).toBeVisible();
21
+ // TODO: verify nothing was submitted downstream for this invalid attempt.
22
+ });
23
+ });
24
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ // Ref: https://playwright.dev/docs/test-parameterize#parameterized-tests
7
+ fixtures_1.test.describe("Forms", () => {
8
+ utils_1.RequiredFields.forEach((targetField) => {
9
+ (0, fixtures_1.test)(`FM-02 Required field validation blocks empty ${targetField.label.source}`, async ({ page, targetPage, }) => {
10
+ await page.goto(targetPage);
11
+ const fieldLocator = page.getByLabel(targetField.label);
12
+ const fieldCount = await fieldLocator.count();
13
+ if (fieldCount === 0) {
14
+ fixtures_1.test.skip(true, `No ${targetField.label.source} field present on this page`);
15
+ }
16
+ await (0, utils_1.fillAllFieldsValidly)(page);
17
+ await fieldLocator.first().clear();
18
+ const submitButtons = page.locator('button[type="submit"], input[type="submit"]');
19
+ await submitButtons.first().click();
20
+ const isErrorPresent = await (0, utils_1.hasVisibleErrorNearField)(fieldLocator.first());
21
+ (0, test_1.expect)(isErrorPresent, `No visible inline error next to the empty ${targetField.label.source} field after submit`).toBe(true);
22
+ const genericError = page.getByText(/form error/i);
23
+ const genericErrorCount = await genericError.count();
24
+ (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);
25
+ });
26
+ });
27
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ fixtures_1.test.describe("Forms - Thank You Confirm", () => {
7
+ (0, fixtures_1.test)("FM-07 Confirmation state matches spec and does not resubmit on Back/Forward", async ({ page, targetPage }) => {
8
+ await page.goto(targetPage);
9
+ await page.getByLabel(/name/i).fill(utils_1.ValidUserInfo.ValidTestName);
10
+ await page.getByLabel(/email/i).fill(utils_1.ValidUserInfo.ValidTestEmail);
11
+ await page.getByLabel(/phone/i).fill(utils_1.ValidUserInfo.ValidTestPhone);
12
+ await page.getByRole("button", { name: /submit/i }).click();
13
+ const confirmation = page.getByText(/thank you|success|confirmation/i);
14
+ await (0, test_1.expect)(confirmation, "Confirmation message/page did not appear or did not match the design spec").toBeVisible();
15
+ let hasResubmitDialog = false;
16
+ page.on("dialog", (dialog) => {
17
+ hasResubmitDialog = true;
18
+ void dialog.dismiss();
19
+ });
20
+ await page.goBack();
21
+ (0, test_1.expect)(hasResubmitDialog, 'Browser Back should NOT show a "resubmit form?" dialog').toBe(false);
22
+ await page.goForward();
23
+ await (0, test_1.expect)(confirmation, "Confirmation should still be shown after pressing Forward").toBeVisible();
24
+ });
25
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../fixtures");
4
+ const test_1 = require("@playwright/test");
5
+ (0, fixtures_1.test)("RS-03 Hero section above the fold", async ({ page, targetPage }) => {
6
+ await page.goto(targetPage);
7
+ const headline = page.getByRole("heading", { level: 1 }).first();
8
+ await (0, test_1.expect)(headline, "Main headline not visible without scrolling").toBeVisible();
9
+ await (0, test_1.expect)(headline, "Main headline not fully within the fold").toBeInViewport({
10
+ ratio: 1,
11
+ });
12
+ const ctaRegex = /get started|book a demo|contact us|try|sign up|request|start|buy|explore|join|register|learn|get|access|download|schedule|book/i;
13
+ const primaryCTA = page
14
+ .getByRole("link", { name: ctaRegex })
15
+ .or(page.getByRole("button", { name: ctaRegex }))
16
+ .first();
17
+ await (0, test_1.expect)(primaryCTA, "Primary CTA not visible without scrolling").toBeVisible();
18
+ await (0, test_1.expect)(primaryCTA, "Primary CTA not fully within the fold").toBeInViewport({
19
+ ratio: 1,
20
+ });
21
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../fixtures");
4
+ const utils_1 = require("../utils");
5
+ const test_1 = require("@playwright/test");
6
+ (0, fixtures_1.test)("RS-08 Images are not blurry/pixelated/stretched, and load an appropriately sized file for the viewport", async ({ page, targetPage, }) => {
7
+ await page.goto(targetPage);
8
+ const imageSizeIssues = await (0, utils_1.findImageSizeIssues)(page);
9
+ const upscaledImages = imageSizeIssues.filter(utils_1.isImageUpscaled);
10
+ (0, test_1.expect)(upscaledImages, "Image(s) appear stretched/upscaled at this viewport").toEqual([]);
11
+ const oversizedImages = imageSizeIssues.filter(utils_1.isImageOversized);
12
+ (0, test_1.expect)(oversizedImages, "Image(s) loaded a file much larger than needed for this viewport — responsive srcset may not be working").toEqual([]);
13
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../fixtures");
4
+ const test_1 = require("@playwright/test");
5
+ fixtures_1.test.describe("Landing Page Automated Smoke Suite", () => {
6
+ (0, fixtures_1.test)("LP-01: Page returns 200, renders H1, and has no console errors", async ({ page, consoleErrors, targetPage, }) => {
7
+ const response = await page.goto(targetPage);
8
+ // Page returns 200
9
+ (0, test_1.expect)(response?.ok()).toBe(true);
10
+ // Renders main heading (H1)
11
+ const h1 = page.getByRole("heading", { level: 1 }).first();
12
+ await (0, test_1.expect)(h1).toBeVisible();
13
+ // No console errors on load
14
+ (0, test_1.expect)(consoleErrors).toHaveLength(0);
15
+ });
16
+ (0, fixtures_1.test)("RS-01: No horizontal overflow (responsive layout stability)", async ({ page, targetPage }) => {
17
+ await page.goto(targetPage);
18
+ const hasHorizontalOverflow = await page.evaluate(() => {
19
+ return document.documentElement.scrollWidth > document.documentElement.clientWidth;
20
+ });
21
+ (0, test_1.expect)(hasHorizontalOverflow).toBe(false);
22
+ });
23
+ (0, fixtures_1.test)("FM-01: Form happy path submits successfully", async ({ page, targetPage, consoleErrors }) => {
24
+ await page.goto(targetPage);
25
+ const forms = page.locator("form");
26
+ const totalFormCount = await forms.count();
27
+ if (totalFormCount === 0) {
28
+ // test.skip aborts execution internally, so no else block is needed
29
+ fixtures_1.test.skip(true, "No form found on the page");
30
+ }
31
+ const allForms = await forms.all();
32
+ await Promise.all(allForms.map(async (form) => {
33
+ // Attempt to fill email inputs
34
+ const emailInputs = await form.getByRole("textbox", { name: /email/i }).all();
35
+ await Promise.all(emailInputs.map(async (emailInput) => {
36
+ const email = `qa+${Date.now().toString()}@manufacanalytics.com`;
37
+ await emailInput.fill(email);
38
+ }));
39
+ // Fill required text inputs
40
+ const textInputs = await form
41
+ .getByRole("textbox")
42
+ .filter({ hasNot: form.getByRole("textbox", { name: /email/i }) })
43
+ .all();
44
+ await Promise.all(textInputs.map(async (textInput) => {
45
+ const isRequired = await textInput.evaluate((element) => {
46
+ let isElementRequired = false;
47
+ if (element instanceof HTMLInputElement) {
48
+ isElementRequired = element.required;
49
+ }
50
+ return isElementRequired;
51
+ });
52
+ if (isRequired) {
53
+ await textInput.fill("Test User");
54
+ }
55
+ }));
56
+ // Set up the network response listener before submitting
57
+ const responsePromise = page.waitForResponse((response) => {
58
+ const method = response.request().method();
59
+ return ["POST", "PUT", "PATCH"].includes(method) || response.request().isNavigationRequest();
60
+ });
61
+ // Submit form
62
+ const submitButton = form.getByRole("button", { name: /submit|send|join|subscribe|save|draft|sign/i });
63
+ const submitButtonCount = await submitButton.count();
64
+ // It is possibility that form might have multiple submit buttons Ref: bud time entry form has 2 submit buttons
65
+ if (submitButtonCount > 0) {
66
+ await submitButton.first().click();
67
+ }
68
+ else {
69
+ await form.evaluate((formElement) => {
70
+ if (formElement instanceof HTMLFormElement) {
71
+ formElement.submit();
72
+ }
73
+ });
74
+ }
75
+ // Wait for the response and verify it was successful
76
+ const response = await responsePromise;
77
+ (0, test_1.expect)(response.ok()).toBe(true);
78
+ }));
79
+ // Brief settle time for post-submit JS (redirects, toasts, error handlers)
80
+ await page.waitForTimeout(1000);
81
+ // No console errors after any form submission
82
+ (0, test_1.expect)(consoleErrors).toHaveLength(0);
83
+ });
84
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ fixtures_1.test.describe("Media Embeds - Carousel Slider", () => {
7
+ (0, fixtures_1.test)("MD-02 Carousel is reachable through all slides without blank slides", async ({ page, isMobile, targetPage, }) => {
8
+ fixtures_1.test.slow();
9
+ await page.goto(targetPage);
10
+ await page.waitForLoadState("load");
11
+ const carousels = page.locator('[data-carousel], [role="region"][aria-roledescription="carousel"]');
12
+ const carouselCount = await carousels.count();
13
+ fixtures_1.test.skip(carouselCount === 0, "No carousel/slider present on this page");
14
+ const carouselsAll = await carousels.all();
15
+ await Promise.all(carouselsAll.map(async (carousel, carouselIndex) => {
16
+ await (0, utils_1.traverseSlidesForward)(carousel, carouselIndex, isMobile);
17
+ if (isMobile) {
18
+ await (0, utils_1.checkSwipeBackward)(carousel, carouselIndex);
19
+ }
20
+ else {
21
+ await (0, utils_1.checkAllDots)(carousel, carouselIndex);
22
+ }
23
+ }));
24
+ const autoAdvanceWaitMS = 30_000;
25
+ await page.waitForTimeout(autoAdvanceWaitMS);
26
+ await Promise.all(carouselsAll.map(async (carousel, carouselIndex) => {
27
+ const isEmptyFrameAfterWait = await (0, utils_1.hasEmptySlideFrame)(carousel);
28
+ (0, test_1.expect)(isEmptyFrameAfterWait, `Carousel at index ${carouselIndex.toString()}: Slide frame was blank after 30s untouched`).toBe(false);
29
+ }));
30
+ });
31
+ });
@@ -0,0 +1,27 @@
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("Media & embeds", () => {
6
+ (0, fixtures_1.test)("MD-04 Cookie consent banner appears on first visit and can be dismissed", async ({ page, targetPage, }) => {
7
+ await page.goto(targetPage);
8
+ // Look for common cookie banner text and an accept button
9
+ const acceptButton = page
10
+ .getByRole("button", { name: /accept|got it|agree/i })
11
+ .filter({ hasText: /cookie|accept/i })
12
+ .first();
13
+ const isBannerVisible = await acceptButton.isVisible();
14
+ // isBannerVisible is true when the cookie consent banner is present and visible on the page
15
+ if (!isBannerVisible) {
16
+ fixtures_1.test.skip(true, "No cookie consent banner found on this page");
17
+ }
18
+ // Dismiss it
19
+ await acceptButton.click();
20
+ // Button/banner should disappear
21
+ await (0, test_1.expect)(acceptButton).toBeHidden();
22
+ // Reload page
23
+ await page.reload();
24
+ // Banner should not reappear (Playwright context persists cookies within a single test by default)
25
+ await (0, test_1.expect)(acceptButton).toBeHidden();
26
+ });
27
+ });
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ fixtures_1.test.describe("Media Embeds - Third Party", () => {
7
+ (0, fixtures_1.test)("MD-03 Third-party embeds render without overflow and remain interactive", async ({ page, targetPage, consoleErrors, }) => {
8
+ await page.goto(targetPage);
9
+ const embeds = page.locator("iframe, embed, object");
10
+ const embedCount = await embeds.count();
11
+ fixtures_1.test.skip(embedCount === 0, "No third-party embeds present on this page");
12
+ const isOverflowing = await (0, utils_1.hasHorizontalScrollbar)(page);
13
+ (0, test_1.expect)(isOverflowing, "Page should not have horizontal overflow with embeds present").toBe(false);
14
+ const collapsedEmbeds = await (0, utils_1.findCollapsedEmbeds)(page);
15
+ (0, test_1.expect)(collapsedEmbeds, `Embed(s) collapsed to zero size:\n${collapsedEmbeds.join("\n")}`).toEqual([]);
16
+ const heading = page.getByRole("heading", { level: 1 }).first();
17
+ await (0, test_1.expect)(heading, "Main heading should remain visible with embeds present").toBeVisible();
18
+ for (let embedIndex = 0; embedIndex < embedCount; embedIndex++) {
19
+ await embeds.nth(embedIndex).click({ timeout: 5_000 });
20
+ }
21
+ (0, test_1.expect)(consoleErrors, `Interacting with an embed should not cause a console error:\n${consoleErrors.join("\n")}`).toHaveLength(0);
22
+ await (0, test_1.expect)(heading, "Main heading should still be visible after interacting with each embed").toBeVisible();
23
+ });
24
+ });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../../fixtures");
4
+ const utils_1 = require("../../utils");
5
+ const test_1 = require("@playwright/test");
6
+ fixtures_1.test.describe("Media Embeds - Video Playback", () => {
7
+ (0, fixtures_1.test)("MD-01 Video playback controls work and autoplay (if present) is muted", async ({ page, targetPage, }) => {
8
+ await page.goto(targetPage);
9
+ const videos = page.locator("video");
10
+ const videoCount = await videos.count();
11
+ fixtures_1.test.skip(videoCount === 0, "No video element present on this page");
12
+ const allVideos = await videos.all();
13
+ await Promise.all(allVideos.map(async (video, index) => {
14
+ const isAutoplaying = await (0, utils_1.isVideoAutoplaying)(video);
15
+ if (isAutoplaying) {
16
+ const isMuted = await (0, utils_1.isVideoMuted)(video);
17
+ (0, test_1.expect)(isMuted, `Autoplaying video at index ${index.toString()} must be muted (autoplay with sound is a failure, especially on mobile)`).toBe(true);
18
+ }
19
+ await video.evaluate((element) => {
20
+ element.pause();
21
+ });
22
+ const isPausedAfterPause = await video.evaluate((element) => {
23
+ return element.paused;
24
+ });
25
+ (0, test_1.expect)(isPausedAfterPause, `Video at index ${index.toString()} should pause when pause is triggered`).toBe(true);
26
+ await video.evaluate((element) => {
27
+ void element.play();
28
+ });
29
+ const isPlayingAfterPlay = await video.evaluate((element) => {
30
+ return !element.paused;
31
+ });
32
+ (0, test_1.expect)(isPlayingAfterPlay, `Video at index ${index.toString()} should play when play is triggered`).toBe(true);
33
+ await video.evaluate((element) => {
34
+ element.muted = false;
35
+ });
36
+ const isUnmuted = await video.evaluate((element) => {
37
+ return !element.muted;
38
+ });
39
+ (0, test_1.expect)(isUnmuted, `Video at index ${index.toString()} should be unmutable via its controls`).toBe(true);
40
+ const initialTime = await video.evaluate((element) => {
41
+ return element.currentTime;
42
+ });
43
+ const seek = initialTime + 1;
44
+ await video.evaluate((element, seekTo) => {
45
+ element.currentTime = seekTo;
46
+ }, seek);
47
+ const currentTimeAfterSeek = await video.evaluate((element) => {
48
+ return element.currentTime;
49
+ });
50
+ const timeDifference = Math.abs(currentTimeAfterSeek - initialTime);
51
+ (0, test_1.expect)(timeDifference, `Video at index ${index.toString()} should have a different current time after seeking`).toBeGreaterThan(0);
52
+ }));
53
+ });
54
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("../../fixtures");
4
+ const sticky_utils_1 = require("../sticky-utils");
5
+ const test_1 = require("@playwright/test");
6
+ (0, fixtures_1.test)("RS-06 Sticky elements (if present) stay pinned and don't cover content or overlap the footer", async ({ page, targetPage, }) => {
7
+ await page.goto(targetPage);
8
+ const { stickyElementCount, flicker, coversContent, overlapsFooter } = await (0, sticky_utils_1.checkStickyElements)(page);
9
+ fixtures_1.test.skip(stickyElementCount === 0, "No sticky/fixed elements present");
10
+ (0, test_1.expect)(flicker, `Sticky element did not stay pinned:\n${flicker.join("\n")}`).toEqual([]);
11
+ (0, test_1.expect)(coversContent, `Sticky element covered body text or form fields:\n${coversContent.join("\n")}`).toEqual([]);
12
+ (0, test_1.expect)(overlapsFooter, `Sticky element overlapped the footer at the bottom of the page:\n${overlapsFooter.join("\n")}`).toEqual([]);
13
+ });
@@ -0,0 +1,13 @@
1
+ import type { Page } from "@playwright/test";
2
+ export interface ScrollMetrics {
3
+ scrollHeight: number;
4
+ scrollY: number;
5
+ }
6
+ export declare function getScrollMetrics(page: Page): Promise<ScrollMetrics>;
7
+ export interface StickyIssues {
8
+ stickyElementCount: number;
9
+ flicker: string[];
10
+ coversContent: string[];
11
+ overlapsFooter: string[];
12
+ }
13
+ export declare function checkStickyElements(page: Page): Promise<StickyIssues>;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getScrollMetrics = getScrollMetrics;
4
+ exports.checkStickyElements = checkStickyElements;
5
+ const utils_1 = require("./utils");
6
+ async function getScrollMetrics(page) {
7
+ return page.evaluate(() => {
8
+ return {
9
+ scrollHeight: document.documentElement.scrollHeight,
10
+ scrollY: window.scrollY,
11
+ };
12
+ });
13
+ }
14
+ // Each page.evaluate call runs in a fresh browser-side context, so a DOM element found in one
15
+ // can't be referenced from the next — this attribute gives each sticky a stable ID to re-select
16
+ // by across the many separate evaluate calls this check makes while scrolling.
17
+ const StickyMarkerAttribute = "data-rs06-sticky-id";
18
+ // Content worth checking that a sticky element doesn't cover: body text and form fields.
19
+ const ContentSelector = "p, li, td, th, label, input, textarea, select, button, a, h1, h2, h3, h4, h5, h6";
20
+ // Marks by computed position alone, not current visibility — a sticky/fixed element that's
21
+ // hidden at page load (e.g. a CTA bar that only appears after scrolling past the hero) must
22
+ // still be tracked once it shows up later, not excluded from the whole test up front.
23
+ async function markStickyElements(page) {
24
+ return page.evaluate((markerAttribute) => {
25
+ const candidates = Array.from(document.querySelectorAll("body *")).filter((element) => {
26
+ const style = window.getComputedStyle(element);
27
+ return style.position === "fixed" || style.position === "sticky";
28
+ });
29
+ candidates.forEach((element, index) => {
30
+ element.setAttribute(markerAttribute, String(index));
31
+ });
32
+ return candidates.length;
33
+ }, StickyMarkerAttribute);
34
+ }
35
+ async function sampleStickyPositions(page) {
36
+ return page.evaluate(({ markerAttribute, contentSelector }) => {
37
+ function isRendered(element) {
38
+ const style = window.getComputedStyle(element);
39
+ const rect = element.getBoundingClientRect();
40
+ return (style.display !== "none" &&
41
+ style.visibility !== "hidden" &&
42
+ Number.parseFloat(style.opacity) !== 0 &&
43
+ rect.width > 0 &&
44
+ rect.height > 0);
45
+ }
46
+ // Only currently-visible stickies contribute a sample this step — a sticky bar that's
47
+ // hidden right now (not yet triggered, or already dismissed) simply isn't sampled here.
48
+ const stickies = Array.from(document.querySelectorAll(`[${markerAttribute}]`)).filter(isRendered);
49
+ const contentElements = Array.from(document.querySelectorAll(contentSelector)).filter(isRendered);
50
+ return stickies.map((element) => {
51
+ const stickyID = Number(element.getAttribute(markerAttribute));
52
+ const rect = element.getBoundingClientRect();
53
+ const desc = `<${element.tagName.toLowerCase()} class="${element.className}">`;
54
+ const covered = contentElements
55
+ .filter((content) => {
56
+ const isNested = element.contains(content);
57
+ const contentRect = content.getBoundingClientRect();
58
+ const overlapWidth = Math.min(rect.right, contentRect.right) - Math.max(rect.left, contentRect.left);
59
+ const overlapHeight = Math.min(rect.bottom, contentRect.bottom) - Math.max(rect.top, contentRect.top);
60
+ return !isNested && overlapWidth > 0 && overlapHeight > 0;
61
+ })
62
+ .map((content) => {
63
+ return `${desc} covers <${content.tagName.toLowerCase()} class="${content.className}">`;
64
+ });
65
+ return { stickyID, top: rect.top, covered };
66
+ });
67
+ }, { markerAttribute: StickyMarkerAttribute, contentSelector: ContentSelector });
68
+ }
69
+ async function collectStickySamples(page, scrollHeight, steps) {
70
+ const samplesByElement = new Map();
71
+ const coversContent = [];
72
+ const down = Array.from({ length: steps + 1 }, (_, i) => {
73
+ return Math.floor((scrollHeight / steps) * i);
74
+ });
75
+ const up = Array.from({ length: steps + 1 }, (_, i) => {
76
+ return Math.floor((scrollHeight / steps) * (steps - i));
77
+ });
78
+ for (const scrollY of [...down, ...up]) {
79
+ await page.evaluate((y) => {
80
+ window.scrollTo(0, y);
81
+ }, scrollY);
82
+ await page.waitForTimeout(100);
83
+ const sample = await sampleStickyPositions(page);
84
+ for (const entry of sample) {
85
+ const samples = samplesByElement.get(entry.stickyID) ?? [];
86
+ samples.push({ scrollY, top: entry.top });
87
+ samplesByElement.set(entry.stickyID, samples);
88
+ coversContent.push(...entry.covered);
89
+ }
90
+ }
91
+ return { samplesByElement, coversContent };
92
+ }
93
+ // A well-behaved sticky/fixed element's top can only change between two samples in one of two
94
+ // ways: it's currently pinned, so top stays put (delta ~0); or it's still in normal flow (not yet
95
+ // engaged), so top moves in exact lockstep with scroll (delta ~ -scrollDelta). Anything else is an
96
+ // unexplained jump — the actual flicker/jank this check is meant to catch.
97
+ function computeFlicker(samplesByElement) {
98
+ const flicker = [];
99
+ for (const [stickyID, samples] of samplesByElement) {
100
+ for (let i = 1; i < samples.length; i++) {
101
+ const topDelta = samples[i].top - samples[i - 1].top;
102
+ const scrollDelta = samples[i].scrollY - samples[i - 1].scrollY;
103
+ const isPinned = Math.abs(topDelta) <= utils_1.ResponsiveConfig.PinFlickerTolerancePX;
104
+ const isFollowingScroll = Math.abs(topDelta + scrollDelta) <= utils_1.ResponsiveConfig.PinFlickerTolerancePX;
105
+ if (!isPinned && !isFollowingScroll) {
106
+ flicker.push(`sticky element #${String(stickyID)} jumped ${Math.abs(topDelta).toFixed(0)}px between consecutive scroll samples`);
107
+ }
108
+ }
109
+ }
110
+ return flicker;
111
+ }
112
+ async function findFooterOverlaps(page) {
113
+ await page.evaluate(() => {
114
+ window.scrollTo(0, document.documentElement.scrollHeight);
115
+ });
116
+ await page.waitForTimeout(100);
117
+ const conflicts = [];
118
+ const footer = await page.$("footer, [role='contentinfo']");
119
+ if (footer !== null) {
120
+ const footerRect = await footer.boundingBox();
121
+ const stickies = await page.$$(`[${StickyMarkerAttribute}]`);
122
+ for (const sticky of stickies) {
123
+ const rect = await sticky.boundingBox();
124
+ if (footerRect === null || rect === null)
125
+ continue;
126
+ const overlapWidth = Math.min(rect.x + rect.width, footerRect.x + footerRect.width) - Math.max(rect.x, footerRect.x);
127
+ const overlapHeight = Math.min(rect.y + rect.height, footerRect.y + footerRect.height) - Math.max(rect.y, footerRect.y);
128
+ if (overlapWidth > 0 && overlapHeight > 0) {
129
+ const description = await sticky.evaluate((element) => {
130
+ return `<${element.tagName.toLowerCase()} class="${element.className}">`;
131
+ });
132
+ conflicts.push(description);
133
+ }
134
+ }
135
+ }
136
+ return conflicts;
137
+ }
138
+ async function checkStickyElements(page) {
139
+ const stickyElementCount = await markStickyElements(page);
140
+ let result;
141
+ if (stickyElementCount === 0) {
142
+ result = { stickyElementCount: 0, flicker: [], coversContent: [], overlapsFooter: [] };
143
+ }
144
+ else {
145
+ const { scrollHeight } = await getScrollMetrics(page);
146
+ const steps = 20;
147
+ const { samplesByElement, coversContent } = await collectStickySamples(page, scrollHeight, steps);
148
+ const flicker = computeFlicker(samplesByElement);
149
+ const overlapsFooter = await findFooterOverlaps(page);
150
+ result = {
151
+ stickyElementCount,
152
+ flicker,
153
+ coversContent: [...new Set(coversContent)],
154
+ overlapsFooter,
155
+ };
156
+ }
157
+ return result;
158
+ }