The mobile contact linework is loading=lazy and only resolved when scrolled into view. Wait for it to finish loading before the geometry assertions read its bounding rect, matching the pattern already used for the Our Work desktop vector.
801 lines
29 KiB
JavaScript
801 lines
29 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import AxeBuilder from "@axe-core/playwright";
|
|
|
|
const homepage = "http://community-works-collaborative.test/";
|
|
|
|
const getBox = async (locator) => {
|
|
const box = await locator.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
return box;
|
|
};
|
|
|
|
const expectNear = (actual, expected, tolerance = 3) => {
|
|
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
|
|
};
|
|
|
|
test.use({ viewport: { width: 402, height: 874 } });
|
|
|
|
test("mobile header preserves its layout and uses the designed hamburger", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const header = await getBox(page.locator(".site-header"));
|
|
const logo = await getBox(page.locator(".site-header__logo"));
|
|
const toggle = page.locator(".nav-main__toggle").first();
|
|
const toggleBox = await getBox(toggle);
|
|
const hamburgerIcon = toggle.locator(".nav-toggle-hamburger svg");
|
|
const closeIcon = toggle.locator(".nav-toggle-x svg");
|
|
|
|
expectNear(header.height, 70, 2);
|
|
expectNear(logo.x, 16, 2);
|
|
expectNear(logo.width, 116, 2);
|
|
expectNear(logo.height, 36, 2);
|
|
expectNear(toggleBox.width, 72, 2);
|
|
expectNear(toggleBox.height, 45, 2);
|
|
await expect(toggle).toHaveCSS(
|
|
"background-color",
|
|
"oklch(0.6353 0.1751 29.61)",
|
|
);
|
|
await expect(toggle).toHaveCSS("border-radius", "22.4px 5.6px");
|
|
await expect(hamburgerIcon).toHaveCSS("fill", "oklch(1 0 0)");
|
|
await expect(closeIcon).toHaveCSS("stroke", "oklch(1 0 0)");
|
|
});
|
|
|
|
test("mobile hero matches the 402px reference composition", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const hero = await getBox(page.locator(".homepage-hero"));
|
|
const heading = await getBox(page.locator(".homepage-hero .intro h1"));
|
|
const vector = await getBox(
|
|
page.locator(".homepage-hero .heroVector .vector"),
|
|
);
|
|
const buttons = page.locator(".homepage-hero .reset .button");
|
|
const firstButton = await getBox(buttons.nth(0));
|
|
const secondButton = await getBox(buttons.nth(1));
|
|
|
|
expectNear(hero.height, 752, 8);
|
|
expectNear(heading.x, 32, 3);
|
|
expect(heading.width).toBeGreaterThanOrEqual(330);
|
|
expectNear(vector.x, -414, 8);
|
|
expectNear(firstButton.y, secondButton.y, 2);
|
|
expectNear(firstButton.width, 148, 4);
|
|
expectNear(secondButton.width, 156, 4);
|
|
|
|
await expect(page.locator(".homepage-hero__location")).toBeVisible();
|
|
});
|
|
|
|
test("mobile hero CTAs remain fully visible at 320px", async ({ page }) => {
|
|
await page.setViewportSize({ width: 320, height: 800 });
|
|
await page.goto(homepage);
|
|
|
|
const vector = await getBox(
|
|
page.locator(".homepage-hero .heroVector .vector"),
|
|
);
|
|
const buttons = page.locator(".homepage-hero .reset .button");
|
|
const firstButton = await getBox(buttons.nth(0));
|
|
const secondButton = await getBox(buttons.nth(1));
|
|
|
|
expectNear(firstButton.y, secondButton.y, 2);
|
|
expect(secondButton.x + secondButton.width).toBeLessThanOrEqual(320);
|
|
expect(vector.x + vector.width).toBeGreaterThanOrEqual(316);
|
|
});
|
|
|
|
test("mobile hero vector covers the 767px composition", async ({ page }) => {
|
|
await page.setViewportSize({ width: 767, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const vector = await getBox(
|
|
page.locator(".homepage-hero .heroVector .vector"),
|
|
);
|
|
|
|
expect(vector.x).toBeLessThan(0);
|
|
expect(vector.x + vector.width).toBeGreaterThanOrEqual(760);
|
|
});
|
|
|
|
test("mobile pull quote matches the reference scale", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
|
|
const quote = await getBox(page.locator(".pull-quote"));
|
|
const text = await getBox(page.locator(".pull-quote__text"));
|
|
const desktopVector = page.locator(".pull-quote__vector--bg");
|
|
const mobileVector = page.locator(".pull-quote__vector--mobile");
|
|
const vector = await getBox(mobileVector);
|
|
const bandHeight = await page
|
|
.locator(".pull-quote")
|
|
.evaluate((element) =>
|
|
Number.parseFloat(getComputedStyle(element, "::after").height),
|
|
);
|
|
|
|
expectNear(quote.height, 361, 6);
|
|
expectNear(bandHeight, 16, 1);
|
|
expectNear(text.x - quote.x, 32, 3);
|
|
expect(text.width).toBeGreaterThanOrEqual(330);
|
|
expectNear(text.height, 216, 6);
|
|
expectNear(
|
|
text.y + text.height / 2,
|
|
quote.y + (quote.height - bandHeight) / 2,
|
|
8,
|
|
);
|
|
expectNear(vector.x, quote.x, 3);
|
|
expectNear(vector.width, quote.width, 3);
|
|
expectNear(vector.height, quote.height - bandHeight, 3);
|
|
await expect(mobileVector).toBeVisible();
|
|
await expect(desktopVector).toBeHidden();
|
|
await expect(mobileVector).toHaveCSS("object-fit", "cover");
|
|
});
|
|
|
|
test("mobile pull quote scales proportionally through 767px", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 767, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const quote = await getBox(page.locator(".pull-quote"));
|
|
const text = await getBox(page.locator(".pull-quote__text"));
|
|
|
|
expectNear(quote.height, 767 * 0.898, 6);
|
|
expect(text.y).toBeGreaterThanOrEqual(quote.y);
|
|
expect(text.y + text.height).toBeLessThanOrEqual(quote.y + quote.height);
|
|
expectNear(text.y + text.height / 2, quote.y + (quote.height - 16) / 2, 8);
|
|
});
|
|
|
|
test("mobile pull quote keeps text above the band at 320px", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 320, height: 800 });
|
|
await page.goto(homepage);
|
|
|
|
const quote = await getBox(page.locator(".pull-quote"));
|
|
const content = await getBox(page.locator(".pull-quote__content"));
|
|
const text = await getBox(page.locator(".pull-quote__text"));
|
|
|
|
expectNear(quote.height, 320 * 0.898, 6);
|
|
expectNear(content.height, quote.height - 16, 3);
|
|
expect(text.y - content.y).toBeGreaterThanOrEqual(12);
|
|
expect(
|
|
content.y + content.height - (text.y + text.height),
|
|
).toBeGreaterThanOrEqual(12);
|
|
});
|
|
|
|
test("mobile pull quote grows when its text needs more room", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const quoteLocator = page.locator(".pull-quote");
|
|
const initialQuote = await getBox(quoteLocator);
|
|
await page.locator(".pull-quote__text p").evaluate((paragraph) => {
|
|
paragraph.textContent = `${paragraph.textContent} ${paragraph.textContent} ${paragraph.textContent}`;
|
|
});
|
|
|
|
const quote = await getBox(quoteLocator);
|
|
const content = await getBox(page.locator(".pull-quote__content"));
|
|
const text = await getBox(page.locator(".pull-quote__text"));
|
|
const vector = await getBox(page.locator(".pull-quote__vector--mobile"));
|
|
|
|
expect(quote.height).toBeGreaterThan(initialQuote.height);
|
|
expectNear(content.height, quote.height - 16, 3);
|
|
expectNear(vector.height, quote.height - 16, 3);
|
|
expect(text.y).toBeGreaterThanOrEqual(content.y);
|
|
expect(text.y + text.height).toBeLessThanOrEqual(
|
|
content.y + content.height,
|
|
);
|
|
});
|
|
|
|
test("pull quote restores the configured artwork at 768px", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 768, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const desktopVector = page.locator(".pull-quote__vector--bg");
|
|
const mobileVector = page.locator(".pull-quote__vector--mobile");
|
|
const quote = await getBox(page.locator(".pull-quote"));
|
|
const vector = await getBox(desktopVector);
|
|
const borderWidth = await page
|
|
.locator(".pull-quote")
|
|
.evaluate((element) =>
|
|
Number.parseFloat(getComputedStyle(element).borderBottomWidth),
|
|
);
|
|
|
|
await expect(desktopVector).toBeVisible();
|
|
await expect(mobileVector).toBeHidden();
|
|
expectNear(vector.x, quote.x, 3);
|
|
expectNear(vector.width, quote.width, 3);
|
|
expectNear(vector.height, quote.height - borderWidth, 3);
|
|
await expect(desktopVector).toHaveCSS("object-fit", "cover");
|
|
});
|
|
|
|
test("mobile pull quote matches the supplied visual reference", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
await expect(page.locator(".pull-quote")).toHaveScreenshot(
|
|
"pullquote-reference.png",
|
|
{
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.071,
|
|
threshold: 0.05,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("mobile pull quote linework render remains stable", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
await page.locator(".pull-quote__content").evaluate((content) => {
|
|
content.style.visibility = "hidden";
|
|
});
|
|
|
|
await expect(page.locator(".pull-quote__vector--mobile")).toHaveScreenshot(
|
|
"pullquote-linework-render.png",
|
|
{
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.001,
|
|
threshold: 0.02,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("mobile media text matches the 402px reference composition", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const blockLocator = page.locator(
|
|
".wp-block-acf-media-text.media-cols.has-two-ctas",
|
|
);
|
|
const block = await getBox(blockLocator);
|
|
const content = await getBox(
|
|
blockLocator.locator(":scope > .content-wrapper"),
|
|
);
|
|
const media = await getBox(blockLocator.locator(":scope > .media-wrapper"));
|
|
const buttons = blockLocator.locator(".reset .button");
|
|
const services = await getBox(buttons.nth(0));
|
|
const about = await getBox(buttons.nth(1));
|
|
const contact = await getBox(page.locator(".contact-block.home-contact"));
|
|
|
|
expect(content.y).toBeLessThan(media.y);
|
|
expectNear(content.y - block.y, 80, 4);
|
|
expectNear(services.y, about.y, 2);
|
|
expectNear(services.width, 148, 4);
|
|
expectNear(about.width, 124, 4);
|
|
expectNear(media.x, 32, 3);
|
|
expectNear(media.y - block.y, 458, 4);
|
|
expectNear(media.width, 338, 3);
|
|
expectNear(media.height, 244, 4);
|
|
expectNear(contact.y - (media.y + media.height), 232, 5);
|
|
});
|
|
|
|
test("media text keeps content first in the DOM and marks its CTA pair", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const block = page.locator(".wp-block-acf-media-text.media-cols");
|
|
const childOrder = await block
|
|
.locator(":scope > div")
|
|
.evaluateAll((children) =>
|
|
children.map((child) =>
|
|
child.classList.contains("content-wrapper")
|
|
? "content"
|
|
: "media",
|
|
),
|
|
);
|
|
|
|
expect(childOrder).toEqual(["content", "media"]);
|
|
await expect(block).toHaveClass(/\bhas-two-ctas\b/);
|
|
});
|
|
|
|
test("mobile media text remains contained at responsive endpoints", async ({
|
|
page,
|
|
}) => {
|
|
for (const width of [320, 767]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const block = page.locator(
|
|
".wp-block-acf-media-text.media-cols.has-two-ctas",
|
|
);
|
|
const content = await getBox(
|
|
block.locator(":scope > .content-wrapper"),
|
|
);
|
|
const media = await getBox(block.locator(":scope > .media-wrapper"));
|
|
const buttons = block.locator(".reset .button");
|
|
const services = await getBox(buttons.nth(0));
|
|
const about = await getBox(buttons.nth(1));
|
|
|
|
expect(content.y).toBeLessThan(media.y);
|
|
expectNear(services.y, about.y, 2);
|
|
expect(media.x).toBeGreaterThanOrEqual(0);
|
|
expect(media.x + media.width).toBeLessThanOrEqual(width);
|
|
expect(services.x).toBeGreaterThanOrEqual(content.x);
|
|
expect(about.x + about.width).toBeLessThanOrEqual(
|
|
content.x + content.width,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("media text keeps its configured ordering at 768px", async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const block = page.locator(".wp-block-acf-media-text.media-cols");
|
|
const contentLocator = block.locator(":scope > .content-wrapper");
|
|
const mediaLocator = block.locator(":scope > .media-wrapper");
|
|
const content = await getBox(contentLocator);
|
|
const media = await getBox(mediaLocator);
|
|
|
|
expect(media.y).toBeLessThan(content.y);
|
|
expectNear(media.x, 70, 3);
|
|
expectNear(media.width, 627, 3);
|
|
await expect(block).toHaveCSS("padding-top", "0px");
|
|
await expect(contentLocator.locator(".font-light")).toHaveCSS(
|
|
"font-size",
|
|
"18px",
|
|
);
|
|
await expect(contentLocator.locator(".reset")).toHaveCSS("display", "flex");
|
|
});
|
|
|
|
test("mobile contact card matches the 402px reference geometry", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const cardLocator = page.locator(".contact-block.home-contact");
|
|
const card = await getBox(cardLocator);
|
|
const heading = await getBox(cardLocator.locator(".contact-block__text"));
|
|
const cta = await getBox(cardLocator.locator(".contact-block__cta"));
|
|
const linework = await getBox(
|
|
cardLocator.locator(".contact-block__linework--mobile"),
|
|
);
|
|
await cardLocator
|
|
.locator(".contact-block__linework--mobile")
|
|
.evaluate(
|
|
(image) =>
|
|
/** @type {HTMLImageElement} */ (image).complete ||
|
|
new Promise((resolve) => {
|
|
/** @type {HTMLImageElement} */ (image).addEventListener(
|
|
"load",
|
|
() => resolve(undefined),
|
|
{ once: true },
|
|
);
|
|
}),
|
|
);
|
|
const backgroundImage = cardLocator.locator(".contact-block__bg-image");
|
|
const bandHeight = await cardLocator.evaluate((element) =>
|
|
Number.parseFloat(getComputedStyle(element).borderBottomWidth),
|
|
);
|
|
|
|
expectNear(card.width, 343, 3);
|
|
expectNear(card.height, 502, 5);
|
|
expectNear(card.x, (402 - card.width) / 2, 2);
|
|
expectNear(bandHeight, 16, 1);
|
|
expectNear(heading.x - card.x, 33, 3);
|
|
expectNear(heading.y - card.y, 31, 4);
|
|
expectNear(cta.x - card.x, 33, 3);
|
|
expectNear(cta.y - card.y, 219, 5);
|
|
expectNear(cta.width, 173, 4);
|
|
expectNear(linework.y - card.y, 299, 5);
|
|
expectNear(linework.height, 188, 5);
|
|
expectNear(linework.width, card.width, 3);
|
|
await expect(backgroundImage).toHaveCSS("object-fit", "cover");
|
|
const overlay = await cardLocator
|
|
.locator(".contact-block__media")
|
|
.evaluate(
|
|
(element) => getComputedStyle(element, "::after").backgroundImage,
|
|
);
|
|
expect(overlay).toContain("linear-gradient");
|
|
await expect(cardLocator).toHaveCSS("border-radius", "16px 16px 0px 0px");
|
|
|
|
const reusableStyles = await cardLocator.evaluate((element) => {
|
|
const clone = element.cloneNode(true);
|
|
clone.classList.remove("home-contact");
|
|
document.body.append(clone);
|
|
|
|
const media = clone.querySelector(".contact-block__media");
|
|
const content = clone.querySelector(".contact-block__content");
|
|
const linework = clone.querySelector(
|
|
".contact-block__linework--mobile",
|
|
);
|
|
const lineworkBox = linework.getBoundingClientRect();
|
|
const styles = {
|
|
borderRadius: getComputedStyle(clone).borderRadius,
|
|
contentPaddingLeft: getComputedStyle(content).paddingLeft,
|
|
lineworkRatio: lineworkBox.width / lineworkBox.height,
|
|
maskImage: getComputedStyle(media).maskImage,
|
|
overlay: getComputedStyle(media, "::after").backgroundImage,
|
|
};
|
|
|
|
clone.remove();
|
|
return styles;
|
|
});
|
|
|
|
expect(reusableStyles.borderRadius).toBe("12px 12px 0px 0px");
|
|
expect(reusableStyles.contentPaddingLeft).toBe("24px");
|
|
expectNear(reusableStyles.lineworkRatio, 342 / 187, 0.03);
|
|
expect(reusableStyles.maskImage).toContain("linear-gradient");
|
|
expect(reusableStyles.overlay).toBe("none");
|
|
});
|
|
|
|
test("mobile contact card scales proportionally and remains contained", async ({
|
|
page,
|
|
}) => {
|
|
for (const width of [320, 767]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const cardLocator = page.locator(".contact-block.home-contact");
|
|
const card = await getBox(cardLocator);
|
|
const heading = await getBox(
|
|
cardLocator.locator(".contact-block__text"),
|
|
);
|
|
|
|
expect(card.x).toBeGreaterThanOrEqual(0);
|
|
expect(card.x + card.width).toBeLessThanOrEqual(width);
|
|
expectNear(card.width / width, 343 / 402, 0.015);
|
|
expectNear(card.height / card.width, 502 / 343, 0.025);
|
|
expectNear((heading.x - card.x) / card.width, 33 / 343, 0.01);
|
|
}
|
|
});
|
|
|
|
test("contact card restores the desktop layout at 768px", async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const card = page.locator(".contact-block.home-contact");
|
|
|
|
await expect(card).toHaveCSS("position", "absolute");
|
|
await expect(card).toHaveCSS("border-bottom-width", "16px");
|
|
await expect(card).toHaveCSS("border-radius", "24px 24px 0px 0px");
|
|
});
|
|
|
|
test("mobile contact card render remains stable", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
|
|
await expect(page.locator(".contact-block.home-contact")).toHaveScreenshot(
|
|
"contact-card-render.png",
|
|
{
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.025,
|
|
threshold: 0.08,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("mobile Our Work contains both reference images", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
|
|
const block = await getBox(page.locator(".our-work"));
|
|
const left = await getBox(page.locator(".our-work__media--left"));
|
|
const bottom = await getBox(page.locator(".our-work__media--bottom"));
|
|
|
|
expectNear(block.height, 888, 12);
|
|
expectNear(left.width, 189, 5);
|
|
expect(bottom.width).toBeGreaterThanOrEqual(335);
|
|
expect(bottom.x).toBeGreaterThanOrEqual(60);
|
|
expect(bottom.x + bottom.width).toBeLessThanOrEqual(402);
|
|
});
|
|
|
|
test("mobile Our Work uses its dedicated linework composition", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const mobileVector = page.locator(".our-work__vector-mobile");
|
|
const mobileImage = mobileVector.locator("img");
|
|
|
|
await expect(mobileVector).toBeVisible();
|
|
await expect(mobileImage).toBeVisible();
|
|
await expect(page.locator(".our-work__vector-desktop")).toBeHidden();
|
|
await mobileImage.evaluate(
|
|
(image) =>
|
|
/** @type {HTMLImageElement} */ (image).complete ||
|
|
new Promise((resolve) => {
|
|
/** @type {HTMLImageElement} */ (image).addEventListener(
|
|
"load",
|
|
() => resolve(undefined),
|
|
{ once: true },
|
|
);
|
|
}),
|
|
);
|
|
expect(
|
|
await mobileImage.evaluate(
|
|
(image) => /** @type {HTMLImageElement} */ (image).currentSrc,
|
|
),
|
|
).toContain("mobile-work-vector.svg");
|
|
});
|
|
|
|
test("mobile Our Work reserves reference height only for both images", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const minHeights = await page.locator(".our-work").evaluate((block) => {
|
|
const measureVariant = (removedClasses) => {
|
|
const clone = block.cloneNode(true);
|
|
clone.classList.remove(...removedClasses);
|
|
if (removedClasses.includes("has-left-image")) {
|
|
clone.querySelector(".our-work__media--left")?.remove();
|
|
}
|
|
if (removedClasses.includes("has-bottom-image")) {
|
|
clone.querySelector(".our-work__media--bottom")?.remove();
|
|
}
|
|
document.body.append(clone);
|
|
const measurements = {
|
|
height: clone.getBoundingClientRect().height,
|
|
minHeight: Number.parseFloat(getComputedStyle(clone).minHeight),
|
|
};
|
|
clone.remove();
|
|
return measurements;
|
|
};
|
|
|
|
return {
|
|
bothImages: {
|
|
height: block.getBoundingClientRect().height,
|
|
minHeight: Number.parseFloat(getComputedStyle(block).minHeight),
|
|
},
|
|
bottomOnly: measureVariant(["has-left-image"]),
|
|
leftOnly: measureVariant(["has-bottom-image"]),
|
|
noImages: measureVariant(["has-left-image", "has-bottom-image"]),
|
|
};
|
|
});
|
|
|
|
expectNear(minHeights.bothImages.minHeight, 888, 3);
|
|
expectNear(minHeights.bothImages.height, 888, 12);
|
|
for (const variant of [
|
|
minHeights.bottomOnly,
|
|
minHeights.leftOnly,
|
|
minHeights.noImages,
|
|
]) {
|
|
expect(variant.minHeight).toBe(0);
|
|
expect(variant.height).toBeLessThan(minHeights.bothImages.height);
|
|
}
|
|
});
|
|
|
|
test("mobile Our Work linework render remains stable", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
|
|
await expect(page.locator(".our-work")).toHaveScreenshot(
|
|
"our-work-linework-render.png",
|
|
{
|
|
animations: "disabled",
|
|
maxDiffPixelRatio: 0.025,
|
|
threshold: 0.08,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("mobile Our Work follows the reference content and overlap order", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const block = await getBox(page.locator(".our-work"));
|
|
const content = await getBox(page.locator(".our-work__content"));
|
|
const left = await getBox(page.locator(".our-work__media--left"));
|
|
const bottom = await getBox(page.locator(".our-work__media--bottom"));
|
|
const paragraph = page.locator(".our-work__content p");
|
|
const paragraphBox = await getBox(paragraph);
|
|
const buttons = page.locator(".our-work .button");
|
|
const primary = await getBox(buttons.nth(0));
|
|
const secondary = await getBox(buttons.nth(1));
|
|
|
|
expectNear(content.x - block.x, 32, 3);
|
|
expectNear(content.y - block.y, 68, 8);
|
|
expectNear(paragraphBox.y - block.y, 146, 4);
|
|
expect(content.y).toBeLessThan(left.y);
|
|
expectNear(left.y - block.y, 428, 12);
|
|
expectNear(bottom.y - block.y, 595, 12);
|
|
expectNear(primary.y, secondary.y, 2);
|
|
expectNear(primary.y - block.y, 321, 4);
|
|
expectNear(primary.height, 44, 2);
|
|
expectNear(secondary.height, 44, 2);
|
|
await expect(paragraph).toHaveCSS("text-wrap", "wrap");
|
|
});
|
|
|
|
test("mobile Our Work remains contained at responsive endpoints", async ({
|
|
page,
|
|
}) => {
|
|
for (const width of [320, 767]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const block = await getBox(page.locator(".our-work"));
|
|
const left = await getBox(page.locator(".our-work__media--left"));
|
|
const bottom = await getBox(page.locator(".our-work__media--bottom"));
|
|
const buttons = page.locator(".our-work .button");
|
|
const primary = await getBox(buttons.nth(0));
|
|
const secondary = await getBox(buttons.nth(1));
|
|
|
|
for (const item of [left, bottom, primary, secondary]) {
|
|
expect(item.x).toBeGreaterThanOrEqual(block.x);
|
|
expect(item.x + item.width).toBeLessThanOrEqual(
|
|
block.x + block.width,
|
|
);
|
|
}
|
|
|
|
expectNear(primary.y, secondary.y, 2);
|
|
expect(left.y).toBeLessThan(bottom.y);
|
|
|
|
const mobileImage = page.locator(".our-work__vector-mobile img");
|
|
await expect(mobileImage).toBeVisible();
|
|
await mobileImage.evaluate(
|
|
(image) =>
|
|
/** @type {HTMLImageElement} */ (image).complete ||
|
|
new Promise((resolve) => {
|
|
/** @type {HTMLImageElement} */ (image).addEventListener(
|
|
"load",
|
|
() => resolve(undefined),
|
|
{ once: true },
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("Our Work restores its tablet layout at 768px", async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const block = page.locator(".our-work");
|
|
const inner = block.locator(".our-work__inner");
|
|
const vector = block.locator(".our-work__vector-desktop img");
|
|
|
|
await expect(block).toHaveCSS("min-height", "0px");
|
|
await expect(block).toHaveCSS("padding-top", "64px");
|
|
await expect(inner).toHaveCSS("padding-left", "38.4px");
|
|
await expect(vector).toHaveCSS("width", "768px");
|
|
await expect(block.locator(".our-work__vector-mobile")).toBeHidden();
|
|
// Force lazy-loaded image to start fetching before reading its source.
|
|
await expect(vector).toBeVisible();
|
|
await vector.evaluate(
|
|
(image) =>
|
|
/** @type {HTMLImageElement} */ (image).complete ||
|
|
new Promise((resolve) => {
|
|
/** @type {HTMLImageElement} */ (image).addEventListener(
|
|
"load",
|
|
() => resolve(undefined),
|
|
{ once: true },
|
|
);
|
|
}),
|
|
);
|
|
expect(
|
|
await vector.evaluate(
|
|
(image) => /** @type {HTMLImageElement} */ (image).currentSrc,
|
|
),
|
|
).toContain("our-work-vector.svg");
|
|
});
|
|
|
|
test("mobile recent posts exposes previous and next controls", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
await expect(
|
|
page.getByRole("button", { name: "Previous article" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Next article" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("mobile recent posts advances to the next article", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
|
|
const cards = page.locator(".recent-posts__card");
|
|
await expect(cards.nth(0)).toBeVisible();
|
|
await expect(cards.nth(1)).toBeHidden();
|
|
|
|
await page.getByRole("button", { name: "Next article" }).click();
|
|
|
|
await expect(cards.nth(0)).toBeHidden();
|
|
await expect(cards.nth(1)).toBeVisible();
|
|
});
|
|
|
|
test("configured footer channels use the prepared SVG icons", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
await expect(
|
|
page.locator('.social-links img[src*="social-facebook.svg"]'),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator('.social-links img[src*="social-linkedin.svg"]'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("mobile footer matches the reference hierarchy", async ({ page }) => {
|
|
await page.goto(homepage);
|
|
|
|
const footer = page.locator(".site-footer");
|
|
const footerBox = await getBox(footer);
|
|
const logo = await getBox(footer.locator(".site-footer__logo-image"));
|
|
const backToTop = await getBox(footer.locator("#backToTopBtn"));
|
|
const nav = await getBox(footer.locator("#footer-nav"));
|
|
const firstNavLink = await getBox(
|
|
footer.locator(".site-footer__nav-link").first(),
|
|
);
|
|
const contact = await getBox(footer.locator(".contact"));
|
|
const firstContactItem = await getBox(
|
|
footer.locator(".contact__item").first(),
|
|
);
|
|
|
|
expectNear(footerBox.height, 673, 12);
|
|
expectNear(logo.y, backToTop.y, 8);
|
|
expect(nav.y).toBeGreaterThan(logo.y + logo.height);
|
|
expect(contact.y).toBeGreaterThan(nav.y + nav.height);
|
|
expectNear(firstNavLink.x, logo.x, 2);
|
|
expectNear(firstNavLink.x, firstContactItem.x, 2);
|
|
});
|
|
|
|
test("mobile footer scales down and remains contained", async ({ page }) => {
|
|
for (const width of [320, 767]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const footer = page.locator(".site-footer");
|
|
const footerBox = await getBox(footer);
|
|
const logo = await getBox(footer.locator(".site-footer__logo-image"));
|
|
const backToTop = await getBox(footer.locator("#backToTopBtn"));
|
|
const nav = await getBox(footer.locator("#footer-nav"));
|
|
const contact = await getBox(footer.locator(".contact"));
|
|
const social = await getBox(footer.locator(".social-links"));
|
|
const copyright = await getBox(footer.locator(".copyright"));
|
|
const expectedHeight = Math.min(width, 402) * (673 / 402);
|
|
|
|
expectNear(footerBox.height, expectedHeight, 12);
|
|
expectNear(logo.y, backToTop.y, 8);
|
|
expect(nav.y).toBeGreaterThan(logo.y + logo.height);
|
|
expect(contact.y).toBeGreaterThan(nav.y + nav.height);
|
|
expect(social.y).toBeGreaterThan(contact.y + contact.height);
|
|
expect(copyright.y).toBeGreaterThan(social.y + social.height);
|
|
expect(footerBox.x).toBeGreaterThanOrEqual(0);
|
|
expect(footerBox.x + footerBox.width).toBeLessThanOrEqual(width);
|
|
expect(
|
|
await page.evaluate(() => document.documentElement.scrollWidth),
|
|
).toBeLessThanOrEqual(width);
|
|
}
|
|
});
|
|
|
|
test("footer restores its four-column desktop layout at 768px", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 768, height: 900 });
|
|
await page.goto(homepage);
|
|
|
|
const footer = page.locator(".site-footer");
|
|
const outer = footer.locator(":scope > .container");
|
|
|
|
await expect(footer).toHaveCSS("border-bottom-width", "16px");
|
|
await expect(footer).toHaveCSS("padding-top", "64px");
|
|
await expect(outer).toHaveCSS("grid-template-columns", /.+ .+ .+ .+/);
|
|
await expect(outer.locator(":scope > .text-right")).toHaveCSS(
|
|
"display",
|
|
"flex",
|
|
);
|
|
});
|
|
|
|
test("mobile homepage has no automated accessibility violations", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(homepage);
|
|
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags([
|
|
"wcag2a",
|
|
"wcag2aa",
|
|
"wcag21a",
|
|
"wcag21aa",
|
|
"wcag22a",
|
|
"wcag22aa",
|
|
])
|
|
.analyze();
|
|
|
|
expect(results.violations).toEqual([]);
|
|
});
|